Compare commits

...

1063 Commits

Author SHA1 Message Date
5fb90c7330 .
All checks were successful
LocalNet_front/pipeline/head This commit looks good
2025-07-14 14:36:10 +09:00
fd1c8c4053 멤버리스트 사진 수정
All checks were successful
LocalNet_front/pipeline/head This commit looks good
2025-07-14 14:30:07 +09:00
90ed8819ad .,,
All checks were successful
LocalNet_front/pipeline/head This commit looks good
2025-04-14 11:12:52 +09:00
130c8fced0 ㅇㅇ
All checks were successful
LocalNet_front/pipeline/head This commit looks good
2025-04-14 10:59:57 +09:00
3804abfa09 .
All checks were successful
LocalNet_front/pipeline/head This commit looks good
2025-04-14 10:54:20 +09:00
1be47c1a58 .
Some checks failed
LocalNet_front/pipeline/head There was a failure building this commit
2025-04-14 10:51:42 +09:00
cb5e274ac1 사원리스트트
All checks were successful
LocalNet_front/pipeline/head This commit looks good
2025-04-11 15:31:44 +09:00
3a0b09624b 마이페이지 수정
All checks were successful
LocalNet_front/pipeline/head This commit looks good
2025-04-11 13:51:36 +09:00
9dfe130500 .
All checks were successful
LocalNet_front/pipeline/head This commit looks good
2025-04-11 13:33:38 +09:00
96411af84a .
All checks were successful
LocalNet_front/pipeline/head This commit looks good
2025-04-11 11:19:29 +09:00
e12e9b8bc8 . 2025-04-11 11:01:42 +09:00
db06418389 아이콘 변경
All checks were successful
LocalNet_front/pipeline/head This commit looks good
2025-04-11 10:44:22 +09:00
549a01d454 알림 없을때 탑바 빨간동그라미 안보이게
All checks were successful
LocalNet_front/pipeline/head This commit looks good
2025-04-11 09:54:41 +09:00
3cdba34130 . 2025-04-11 09:48:09 +09:00
d3ba7d446e 유튜브 첨부 형식 추가.
All checks were successful
LocalNet_front/pipeline/head This commit looks good
2025-04-11 00:37:41 +09:00
cca27b9583 사원등록 실패시 토글리셋 수정 2025-04-10 23:24:02 +09:00
3d147076ef 퇴근취소시에 탑바 셀렉트박스 바로 안따라옴 2025-04-10 21:12:41 +09:00
e75ca56f7d . 2025-04-10 16:24:20 +09:00
5be05bbab6 .
All checks were successful
LocalNet_front/pipeline/head This commit looks good
2025-04-10 15:59:11 +09:00
93b8843dd7 .
All checks were successful
LocalNet_front/pipeline/head This commit looks good
2025-04-10 15:50:15 +09:00
2bd64142ac .. 2025-04-10 15:44:30 +09:00
4c5b4481b6 Merge branch '250410_park' 2025-04-10 15:23:17 +09:00
79ce960a3a 사원 등록에서 로그인 세션으로 등록/반려가 아닌 DB의 권한을 가져와서 처리하는 방향으로 수정 2025-04-10 15:23:02 +09:00
888a733f4b Merge branch 'main' into mypage 2025-04-10 15:15:30 +09:00
8361a02dc8 Merge branch 'main' into mypage 2025-04-10 15:15:21 +09:00
11ebea8ccd 권한부여 수정 2025-04-10 15:14:53 +09:00
103f5f3a62 Merge branch 'khj' 2025-04-10 15:11:38 +09:00
14c8fb4108 프로필이미지 꽉차게->USER-AVATAR PADDING없애기 2025-04-10 15:10:17 +09:00
803e6da4b3 유저 승인 api 변경
All checks were successful
LocalNet_front/pipeline/head This commit looks good
2025-04-10 13:42:32 +09:00
ba9a752250 게시판 동영상 수정 2025-04-10 13:28:50 +09:00
5c7f7c6346 Merge branch 'khj' 2025-04-10 10:30:01 +09:00
5b24a0254b 코드수정정 2025-04-10 10:29:37 +09:00
52c3bbdf6c 1
All checks were successful
LocalNet_front/pipeline/head This commit looks good
2025-04-09 08:53:43 +09:00
a27de5443a 일반 영상 url 도 첨부되도록 수정 2025-04-09 08:31:59 +09:00
1b354d464c 비디오
All checks were successful
LocalNet_front/pipeline/head This commit looks good
2025-04-08 21:22:33 +09:00
23525d5ba1 영상 링크 기능
All checks were successful
LocalNet_front/pipeline/head This commit looks good
2025-04-08 20:40:25 +09:00
632c421ec1 1 2025-04-08 20:25:31 +09:00
94356aba09 휴가 클릭시 좌측 메뉴에 선택 활성화 되게 2025-04-08 20:23:41 +09:00
yoon
fc6c828624 데이터피커추가 2025-04-08 19:23:58 +09:00
yoon
437592ed0d selectbox 프로젝트 선택 변경 사항 감지 추가 2025-04-08 19:09:37 +09:00
yoon
affc1f4b59 알림 2025-04-08 18:52:49 +09:00
yoon
5667c3edf8 프로젝트 설명 없을때 하이픈 및 삭제 컨펌 2025-04-08 18:45:27 +09:00
yoon
8df2674755 멤버 없을 때 하이픈 2025-04-08 18:44:41 +09:00
f37c8ec947 투표컨펌 추가가 2025-04-08 16:13:07 +09:00
15104c2f44 Merge branch 'main' of http://192.168.0.251:3000/localnet/localhost-front 2025-04-08 15:50:00 +09:00
3c54cea558 투표 컨펌펌 2025-04-08 15:49:59 +09:00
4f9a879083 .
All checks were successful
LocalNet_front/pipeline/head This commit looks good
2025-04-08 14:37:13 +09:00
70143f3174 . 2025-04-08 14:30:44 +09:00
10068bb1c7 .
All checks were successful
LocalNet_front/pipeline/head This commit looks good
2025-04-08 14:28:33 +09:00
8c7b82d0ae d위치수정정
All checks were successful
LocalNet_front/pipeline/head This commit looks good
2025-04-08 14:19:50 +09:00
028c5bda11 수정 날짜
All checks were successful
LocalNet_front/pipeline/head This commit looks good
2025-04-08 13:52:44 +09:00
8321793a31 Merge branch 'khj'
All checks were successful
LocalNet_front/pipeline/head This commit looks good
2025-04-08 13:40:22 +09:00
10a955f13f 줄바꿈수정정 2025-04-08 13:39:54 +09:00
f21973705e Merge branch '250408_park'
All checks were successful
LocalNet_front/pipeline/head This commit looks good
2025-04-08 11:28:20 +09:00
cc01b95350 좋아요 싫어요 watch 함수 추가 2025-04-08 11:28:07 +09:00
cf88671869 공휴일에는 이벤트 못넣게 수정 2025-04-08 11:03:26 +09:00
b281b38351 Merge branch 'main' of http://192.168.0.251:3000/localnet/localhost-front
All checks were successful
LocalNet_front/pipeline/head This commit looks good
2025-04-08 10:57:49 +09:00
3f32c573a7 . 2025-04-08 10:57:47 +09:00
4772077cc1 Merge branch 'khj' 2025-04-08 10:57:30 +09:00
85c06185ca 이미지 클래스 추가가 2025-04-08 10:56:56 +09:00
1d893200cb Merge branch 'main' of http://192.168.0.251:3000/localnet/localhost-front 2025-04-08 10:52:13 +09:00
a7c588986b . 2025-04-08 10:52:12 +09:00
0f1b3fb4d7 Merge branch '250408_park'
All checks were successful
LocalNet_front/pipeline/head This commit looks good
2025-04-08 10:47:33 +09:00
c01e45759d 이벤트 타임피커 영역 누를때 시간선택 가능하게 수정 2025-04-08 10:47:13 +09:00
27ab492b45 늘어짐 수정정
All checks were successful
LocalNet_front/pipeline/head This commit looks good
2025-04-08 10:45:46 +09:00
446f9925c8 Merge branch 'main' of http://192.168.0.251:3000/localnet/localhost-front
All checks were successful
LocalNet_front/pipeline/head This commit looks good
2025-04-08 10:39:14 +09:00
a1b304a159 달력 버튼 커서 2025-04-08 10:39:11 +09:00
a6fdcf9013 Merge branch 'khj' 2025-04-08 10:38:35 +09:00
4a8414a1c4 이미지 클래스 추가 / 용어집 영역 설정정 2025-04-08 10:36:47 +09:00
2469d3ec88 날씨 로딩중일떄 글씨 안보이게 2025-04-08 10:33:13 +09:00
6bce7f6e38 프로필 style 수정 2025-04-08 10:30:01 +09:00
683e06424e 탑바 영역 수정 2025-04-08 10:10:46 +09:00
8fec088bfa . 2025-04-08 10:04:35 +09:00
7a37f837d6 날씨 스토리지 2025-04-08 10:01:09 +09:00
9af35ff2d8 Merge branch 'main' into vacation 2025-04-08 09:52:07 +09:00
f18bc15d91 날씨 정보 커스텀 이미지 추가 및 각 라우터 name 설정
All checks were successful
LocalNet_front/pipeline/head This commit looks good
2025-04-08 01:11:39 +09:00
c43614c743 메인 달력 날씨 업데이트 2025-04-08 00:54:59 +09:00
1c483ef727 게시글 상세 목록으로 버튼 추가 2025-04-07 20:43:33 +09:00
9e578109e1 Merge branch '250407_park' 2025-04-07 20:11:00 +09:00
6f91dd9379 메인페이지 휴가이벤트 누르면 휴가페이지로 이동 2025-04-07 20:10:47 +09:00
yoon
4635c9b372 . 2025-04-07 16:54:06 +09:00
yoon
1d6bc44680 반명항 기준 프로필 이미지 수정 2025-04-07 16:46:51 +09:00
ffc8b44b46 날씨 스토리지에저장 2025-04-07 16:15:49 +09:00
64bba660cd Merge branch 'main' of http://192.168.0.251:3000/localhost/localhost-front
All checks were successful
LocalNet_front/pipeline/head This commit looks good
2025-04-07 15:54:19 +09:00
5e1c8795e6 용어집 영억,짤림 수정정 2025-04-07 15:53:53 +09:00
17c3aefd2b 용어집 영역 짤림 /글 작성시 계속 입력할때 옆으로 계속 늘어남 수정정 2025-04-07 15:53:07 +09:00
6ee96f3abd Merge branch '250407_park3' 2025-04-07 15:43:40 +09:00
ee3027b3e1 댓글 버튼 active 관련 수정 2025-04-07 15:43:23 +09:00
0a0c7b4244 . 2025-04-07 15:38:37 +09:00
25c691338d 수정사항 2025-04-07 15:37:58 +09:00
1f9f9bd1ff .. 2025-04-07 15:01:38 +09:00
a02a316370 Merge branch 'khj'
All checks were successful
LocalNet_front/pipeline/head This commit looks good
2025-04-07 13:40:35 +09:00
2b319c99b8 토탈수 변경경 2025-04-07 13:39:53 +09:00
594568ed2d ..
All checks were successful
LocalNet_front/pipeline/head This commit looks good
2025-04-07 13:37:14 +09:00
e61ed9bc87 obecjt-fit contain 적용
All checks were successful
LocalNet_front/pipeline/head This commit looks good
2025-04-07 13:25:41 +09:00
348fd83642 profile img 클래스 css 제거(중복)
All checks were successful
LocalNet_front/pipeline/head This commit looks good
2025-04-07 13:20:26 +09:00
29f97e8aba 당일 날짜에 이벤트 입력 후 디폴트 색깔 없어짐 수정
All checks were successful
LocalNet_front/pipeline/head This commit looks good
2025-04-07 13:10:23 +09:00
84c4e2a7ad 라우트 로딩 후 app 마우트 되도록 수정
All checks were successful
LocalNet_front/pipeline/head This commit looks good
2025-04-07 12:55:11 +09:00
fc32aba4ab 123
All checks were successful
LocalNet_front/pipeline/head This commit looks good
2025-04-07 11:50:19 +09:00
0a3f639d06 게시글 상세 화면 로딩 시 스크롤 매위로 2025-04-07 11:33:36 +09:00
19c574cf9d 이미지 높이 수정 2025-04-07 11:26:56 +09:00
feb10ebae5 사원등록 mbti 추가 2025-04-07 11:13:11 +09:00
2e94e96d5f Merge branch 'khj' 2025-04-07 10:46:30 +09:00
81f7e15604 더보기 ㅁ버튼 변경경 2025-04-07 10:46:09 +09:00
d2d0f2892d 댓글 수정시 maxlength 500 2025-04-07 10:43:21 +09:00
b4d8d09986 Merge branch '250407_main_park'
All checks were successful
LocalNet_front/pipeline/head This commit looks good
2025-04-07 10:37:21 +09:00
135e16e709 수정버튼 클래스 수정 2025-04-07 10:36:41 +09:00
47c28030cf Merge branch 'main' into board-ji 2025-04-07 10:35:38 +09:00
7e02512011 Merge branch 'main' into board-ji 2025-04-07 10:35:15 +09:00
eb39a2a0b7 수정사항 2025-04-07 10:34:49 +09:00
c1a45f7d02 g 2025-04-07 10:25:48 +09:00
34f6bad788 Merge branch 'khj' 2025-04-07 10:25:03 +09:00
d2864f7ac0 용어집 수정정 2025-04-07 10:24:39 +09:00
3be8560654 이벤트 폰트 크기 변경 2025-04-07 10:20:05 +09:00
08331b7b58 공휴일 style 달력 좌우로 넓게 나오게 2025-04-05 10:03:15 +09:00
b8a7310af0 프로필 이미지 나오게, 아무기능없는 포인터 제거 2025-04-05 00:59:37 +09:00
44cec4cccd 로그인 페이지에 날씨정보 안가져오게
All checks were successful
LocalNet_front/pipeline/head This commit looks good
2025-04-05 00:25:25 +09:00
08d30c7fc4 유효성 체크 및 경고 문구 변경 2025-04-04 23:34:57 +09:00
04f4346122 1 2025-04-04 22:59:36 +09:00
152d3c6fa9 게시글 수정화면에서 콘솔로그 제거 2025-04-04 22:59:33 +09:00
yoon
bba87fe02a select 2025-04-04 19:21:39 +09:00
yoon
da4e069b59 위치수정ㅇ 2025-04-04 17:20:57 +09:00
yoon
b24c6d85e4 뒤로가기 버튼 2025-04-04 17:06:42 +09:00
yoon
3e873b7861 Merge branch 'main' into commuters 2025-04-04 16:47:02 +09:00
yoon
9f494d9d12 위치변경 2025-04-04 16:41:36 +09:00
yoon
5f9a99dd02 컬러 2025-04-04 16:35:37 +09:00
yoon
17e86f49d0 컬러, 휴대전화 2025-04-04 16:32:53 +09:00
yoon
f18aa4dc16 모달 닫을 때 맵 닫기 2025-04-04 16:32:27 +09:00
d1aa3d3a04 Merge branch 'main' into mypage 2025-04-04 16:20:44 +09:00
b33f38b5a5 Merge branch 'main' into mypage 2025-04-04 16:20:34 +09:00
2ffd88cb68 Merge branch 'main' of http://192.168.0.251:3000/localnet/localhost-front 2025-04-04 16:18:37 +09:00
c4b40dea79 용어집 수정사항 없을떄 저장 x 2025-04-04 16:18:35 +09:00
5390988afa .. 2025-04-04 16:17:13 +09:00
080511f428 메인보드 2025-04-04 16:14:42 +09:00
582030fd5d 탑바수정 2025-04-04 16:04:30 +09:00
yoon
ba12c0ad2d cursor-pointer 2025-04-04 14:49:32 +09:00
yoon
358f4a343a 컬러 없을 때 2025-04-04 14:48:09 +09:00
yoon
142b576804 남은 컬러 없는 경우 2025-04-04 14:47:56 +09:00
d792253292 t
All checks were successful
LocalNet_front/pipeline/head This commit looks good
2025-04-04 14:22:17 +09:00
03aba73bdb 이미지꺠짐 수정정
All checks were successful
LocalNet_front/pipeline/head This commit looks good
2025-04-04 14:09:31 +09:00
a438965787 Merge branch 'khj' 2025-04-04 14:04:47 +09:00
bb9a140aa9 수정정 2025-04-04 14:04:10 +09:00
daa3bb9921 탑뷰수정
All checks were successful
LocalNet_front/pipeline/head This commit looks good
2025-04-04 13:30:38 +09:00
4907a12e65 t 2025-04-04 13:24:14 +09:00
87465c01fa Merge branch 'khj' 2025-04-04 13:22:43 +09:00
a0c2aa04ad 투표 수정정 2025-04-04 13:22:16 +09:00
3f37370d0d 에민 보드 폰트크기 수정
All checks were successful
LocalNet_front/pipeline/head This commit looks good
2025-04-04 13:21:38 +09:00
f6365e6b31 입력 전처리
All checks were successful
LocalNet_front/pipeline/head This commit looks good
2025-04-04 13:08:20 +09:00
b2deb1ade2 Merge branch 'main' into mypage 2025-04-04 12:57:41 +09:00
a6c57646d7 Merge branch 'main' into mypage 2025-04-04 12:57:29 +09:00
819a82cd89 // 2025-04-04 12:56:53 +09:00
a27eda7124 . 2025-04-04 11:40:52 +09:00
4d8e4f7b3e . 2025-04-04 11:39:59 +09:00
5a60012ce6 컬러,전화번호 중복체크 2025-04-04 11:36:04 +09:00
c60bed282f 마이페이지 2025-04-04 11:08:05 +09:00
a97d3b4609 Merge branch 'khj' 2025-04-04 10:38:14 +09:00
78eefe5a7f 투표 수정정 2025-04-04 10:37:48 +09:00
e4b25680da Merge branch '250403_board_commentModify_park' 2025-04-03 22:18:08 +09:00
7e0ae7063d boardview 에서 수정, 삭제시 버튼 클릭 활성화 2025-04-03 22:17:30 +09:00
1fef343fd9 종료수정정 2025-04-03 16:18:50 +09:00
ea6b5d26c9 Merge branch 'khj' 2025-04-03 16:13:29 +09:00
36888c3e30 투표하기 모달 2025-04-03 16:12:57 +09:00
a5fe714c73 마이페이지 2025-04-03 15:51:30 +09:00
2072a41ca9 로그인창에서 새로고침시 날씨 401에러나는거 수정 2025-04-03 12:17:08 +09:00
29ee90a84e 투표용어 메인수정정
All checks were successful
LocalNet_front/pipeline/head This commit looks good
2025-04-03 11:17:11 +09:00
fb5a0d6363 날짜 스타일변경경 2025-04-03 11:03:27 +09:00
9548025afd 이미지수정정 2025-04-03 10:59:46 +09:00
55c0c33894 Merge branch 'khj' 2025-04-03 10:44:14 +09:00
60df96cf12 투표 메인 수정정 2025-04-03 10:43:51 +09:00
3b45ae929e 게시판 리스트 수정 2025-04-03 10:39:07 +09:00
53b4519985 Merge branch 'main' into vacation 2025-04-03 10:35:23 +09:00
4cd2ddb102 Merge branch 'main' into vacation 2025-04-03 10:35:11 +09:00
8b2262d633 당일 css 수정 2025-04-03 10:30:49 +09:00
e1e8c27db0 Merge branch 'main' of http://192.168.0.251:3000/localnet/localhost-front 2025-04-03 10:13:34 +09:00
2e5ce81981 Merge branch 'khj' 2025-04-03 10:13:32 +09:00
26c3f3ec41 용어집 스타일 수정 2025-04-03 10:13:06 +09:00
caba716ac6 Merge branch 'main' of http://192.168.0.251:3000/localhost/localhost-front 2025-04-03 10:12:49 +09:00
e1cc692085 메인 달력 사원 이미지 url 수정 2025-04-03 10:12:46 +09:00
3cf6aeab9a Merge branch 'main' of http://192.168.0.251:3000/localnet/localhost-front 2025-04-03 10:07:07 +09:00
7e2caa5f09 . 2025-04-03 10:07:05 +09:00
ff0ea598ec 메인달력 STYLE 이 다른곳에 적용안되게 수정, 2025-04-03 10:02:50 +09:00
4c5da13291 메인달력 오류 수정
All checks were successful
LocalNet_front/pipeline/head This commit looks good
2025-04-01 19:56:39 +09:00
61767d6b68 main 달력 css 수정 2025-04-01 18:51:03 +09:00
fabda99d78 수정 2025-04-01 16:20:17 +09:00
b348fb14ae Merge branch 'khj' 2025-04-01 16:17:17 +09:00
61a3041a5e 메인 -용어집 투표진행 추가가 2025-04-01 16:16:42 +09:00
a42b5ac191 . 2025-04-01 15:45:59 +09:00
6357a8eb44 아이콘 반응형 2025-04-01 15:45:02 +09:00
9785b96de6 메인 수정 2025-04-01 15:32:19 +09:00
2cb2c81f84 . 2025-04-01 15:29:12 +09:00
58cbb8f534 Merge branch 'main' of http://192.168.0.251:3000/localhost/localhost-front 2025-04-01 15:24:55 +09:00
0bf6b99189 1 2025-04-01 15:24:54 +09:00
869423c764 맑음 이미지 변환 2025-04-01 15:21:22 +09:00
81909fa6e0 . 2025-04-01 14:50:55 +09:00
e5acfa5cf3 날씨 api 백엔드로 수정 2025-04-01 14:45:21 +09:00
f975273c47 탑바수정
Some checks failed
LocalNet_front/pipeline/head There was a failure building this commit
2025-04-01 14:18:26 +09:00
6f02921c12 Merge branch 'mypage'
Some checks failed
LocalNet_front/pipeline/head There was a failure building this commit
2025-04-01 14:12:34 +09:00
6ac05b2a99 Merge branch 'mypage' 2025-04-01 14:12:23 +09:00
37dc13a92a 탑바 날씨 2025-04-01 14:11:15 +09:00
51e3065400 오늘 날짜 클릭시 초록색 -> 하늘색 2025-04-01 14:08:58 +09:00
453f1d46f7 Merge branch '250401_main_park' 2025-04-01 13:10:41 +09:00
b6e2623fe3 사원 등록 api 작업 완료 2025-04-01 13:10:19 +09:00
c6f2743d82 . 2025-04-01 11:44:45 +09:00
5cb43eac9f 리스트...추가,공지좋아요,댓글 없애기 2025-04-01 11:22:07 +09:00
ebad7d3d5f dd 2025-04-01 11:13:20 +09:00
f2a9ad693f 메인병합 2025-04-01 10:58:14 +09:00
13cf6e56ab Merge branch 'main' into mypage 2025-04-01 10:55:25 +09:00
05769d18ff .. 2025-04-01 10:54:43 +09:00
675eb93587 메인보드 2025-04-01 10:52:24 +09:00
826aeca42a Merge branch 'khj' 2025-04-01 10:27:33 +09:00
5abf73892b 메인 컴포넌트 추가가 2025-04-01 10:27:03 +09:00
0e901ccaa0 Merge branch 'main' into mypage 2025-04-01 09:46:37 +09:00
7a0df53121 Merge branch 'main' into mypage 2025-04-01 09:46:30 +09:00
3e3849d0d1 . 2025-04-01 09:44:17 +09:00
e9f3a6c8a6 게시글 수정 원복 2025-03-31 20:24:03 +09:00
yoon
9e2b3a072d boder-color 추가 2025-03-31 19:10:50 +09:00
yoon
551569c1c6 뒤로가기 수정 2025-03-31 18:47:26 +09:00
yoon
12297153e4 map 2025-03-31 17:20:16 +09:00
yoon
19777906a4 비번 체크 2025-03-31 16:46:39 +09:00
f054467887 내가쓴 투표일때 이름밀림 수정정 2025-03-31 16:06:10 +09:00
2e596009ad 게시판 메인인 2025-03-31 16:01:05 +09:00
02ec5271b3 프로필 크기 크게 변경경 2025-03-31 15:57:29 +09:00
f0ff9ced0d 메인 2025-03-31 15:50:42 +09:00
7f6caed69e Merge branch 'main' into mypage 2025-03-31 15:46:06 +09:00
931c4b6c9a 마이페이지지 2025-03-31 14:52:55 +09:00
666c081813 카드 크기 변경
All checks were successful
LocalNet_front/pipeline/head This commit looks good
2025-03-31 14:00:41 +09:00
174867fc35 장소 입력 maxlength 20 2025-03-31 13:48:05 +09:00
8375ffee38 . 2025-03-31 13:37:28 +09:00
330f05e92b 사원등록 if 조건 변경 2025-03-31 13:32:01 +09:00
ac7b4d558e 수정
All checks were successful
LocalNet_front/pipeline/head This commit looks good
2025-03-31 13:29:30 +09:00
1c57198839 오류 수정 2025-03-31 13:25:31 +09:00
yoon
6430d45279 Merge branch 'main' of http://192.168.0.251:3000/localnet/localhost-front
All checks were successful
LocalNet_front/pipeline/head This commit looks good
2025-03-31 13:14:13 +09:00
yoon
7c485dc711 위치 로직 수정, 퇴근위치 설정 2025-03-31 13:14:09 +09:00
61096b84f2 config
All checks were successful
LocalNet_front/pipeline/head This commit looks good
2025-03-31 13:12:00 +09:00
f5038a5f8e Merge branch 'workMain'
All checks were successful
LocalNet_front/pipeline/head This commit looks good
2025-03-31 13:04:18 +09:00
972fd6ae96 1303 2025-03-31 13:03:45 +09:00
yoon
09fd2df838 Merge branch 'main' of http://192.168.0.251:3000/localnet/localhost-front
All checks were successful
LocalNet_front/pipeline/head This commit looks good
2025-03-31 12:22:03 +09:00
yoon
ceada27663 오타 수정 및 아이디 길이 체크 추가 2025-03-31 12:21:57 +09:00
c1274cf9a0 메인 이벤트 달력 2025-03-31 11:22:26 +09:00
95d1547400 Merge branch 'khj' 2025-03-31 11:19:52 +09:00
c0801c20a3 종료투표색변경경 2025-03-31 11:19:21 +09:00
18f47fff5f . 2025-03-31 11:11:43 +09:00
68607e3d1f .
All checks were successful
LocalNet_front/pipeline/head This commit looks good
2025-03-31 11:02:54 +09:00
9811ed7ca2 .
All checks were successful
LocalNet_front/pipeline/head This commit looks good
2025-03-31 11:00:47 +09:00
c139358135 휴가 왕관 수정
All checks were successful
LocalNet_front/pipeline/head This commit looks good
2025-03-31 10:54:56 +09:00
fcb8d4535e 휴가 왕관 아이콘
All checks were successful
LocalNet_front/pipeline/head This commit looks good
2025-03-31 10:26:12 +09:00
e270b0bb88 파일선택 > 삭제했다가 다시 파일선택시 안들어감 2025-03-28 18:53:40 +09:00
9c28054001 Merge branch 'main' of http://192.168.0.251:3000/localhost/localhost-front 2025-03-28 18:50:43 +09:00
8d1748548e 파일선택 > 삭제했다가 다시 파일선택시 안들어감 2025-03-28 18:48:23 +09:00
yoon
5b82d3d315 Merge branch 'main' into commuters 2025-03-28 17:05:31 +09:00
yoon
da13750404 map 2025-03-28 17:04:45 +09:00
ecb321c9fb Merge branch 'main' into board-ji 2025-03-28 16:34:01 +09:00
5c5a2c63ef Merge branch 'main' into board-ji 2025-03-28 16:33:49 +09:00
9035d339ac 수정 에디터 2025-03-28 16:32:53 +09:00
ad3af31e37 검색 필터링 유지 2025-03-28 16:18:06 +09:00
yoon
5af68dcf3d Merge branch 'main' into commuters
All checks were successful
LocalNet_front/pipeline/head This commit looks good
2025-03-28 15:59:04 +09:00
yoon
657312b473 map 공통 파일 뺴기 미완성 2025-03-28 15:58:33 +09:00
yoon
c90ce680fc min 삭제 2025-03-28 15:57:56 +09:00
d47ee95f56 공지 좋아요,댓글 없게 2025-03-28 15:23:34 +09:00
2c0a6f5ffc Merge branch 'main' of http://192.168.0.251:3000/localnet/localhost-front 2025-03-28 15:22:08 +09:00
969f566187 삭제된것 디세이블 투표표 2025-03-28 15:22:06 +09:00
yoon
525a484615 스타일 삭제 2025-03-28 15:15:44 +09:00
yoon
48dd6c5b9c 비밀번호 4자리 alert 누락 추가 2025-03-28 15:11:00 +09:00
yoon
51f750ae1c Merge branch 'main' into commuters 2025-03-28 15:02:57 +09:00
yoon
89f151d7bc 커서추가 2025-03-28 15:02:19 +09:00
yoon
767c160acd kakao 확인 2025-03-28 14:04:53 +09:00
21165d1d54 Merge branch '250328_comment'
All checks were successful
LocalNet_front/pipeline/head This commit looks good
2025-03-28 14:00:01 +09:00
7fd115ddc7 댓글 수정사항 없으면 submit 버튼 disabled 처리 2025-03-28 13:59:38 +09:00
f733569211 Merge branch 'khj'
All checks were successful
LocalNet_front/pipeline/head This commit looks good
2025-03-28 13:44:15 +09:00
7485b6e7c8 종료날짜 vaild 추가가 2025-03-28 13:43:51 +09:00
2baebdf1e9 Merge branch 'main' of http://192.168.0.251:3000/localhost/localhost-front 2025-03-28 13:21:59 +09:00
750cbf4b74 1 2025-03-28 13:21:58 +09:00
83abdded54 Merge branch 'main' into board-ji 2025-03-28 13:19:48 +09:00
008adac7a5 Merge branch 'main' into board-ji 2025-03-28 13:19:35 +09:00
1dcde5eb6b 글수정 2025-03-28 13:17:33 +09:00
9bfd16efa2 Merge branch '250328_like_dislike' 2025-03-28 13:08:36 +09:00
ad4bcb511f 댓글 사용자 좋아요 싫어요 상태 업데이트 2025-03-28 13:08:21 +09:00
fdb1f6f339 Merge branch '250328_like_dislike' 2025-03-28 12:28:23 +09:00
3f8718831f 사용자의 게시글 좋아요 싫어요 상태 업데이트 2025-03-28 12:27:26 +09:00
d4bfc164a4 공지 복구 2025-03-28 11:19:41 +09:00
e447968ecf 게시글 수정 시 상세 페이지로 이동 하도록 변경 2025-03-28 10:29:11 +09:00
3b50c84118 Merge branch 'main' of http://192.168.0.251:3000/localhost/localhost-front 2025-03-28 10:08:29 +09:00
416ec12a20 250328 10am 2025-03-28 10:08:22 +09:00
6c7ccff8ea 공지사항 댓글,좋아요기능 삭제 2025-03-28 10:07:13 +09:00
a540feb851 250328_work 2025-03-28 09:22:44 +09:00
4a8f74c357 Merge branch 'main' of http://192.168.0.251:3000/localhost/localhost-front 2025-03-28 01:10:20 +09:00
de04cf679b 검색/페이징/필터링 후 수정시 검색어/페이징 풀림 2025-03-28 01:08:01 +09:00
yoon
4395be2e90 달력 모달 2025-03-27 18:53:44 +09:00
yoon
9e468d6ea9 Merge branch 'main' into commuters
All checks were successful
LocalNet_front/pipeline/head This commit looks good
2025-03-27 18:12:55 +09:00
yoon
dd268c0df6 퇴근지 추가 2025-03-27 18:12:11 +09:00
42987e8554 공지 댓글 삭제 2025-03-27 16:22:17 +09:00
yoon
8dcb69bcb6 select 디폴트옵션 이상한거 수정 2025-03-27 15:54:52 +09:00
6e14790e41 Merge branch 'khj' 2025-03-27 15:42:11 +09:00
09afafdab1 수정정 2025-03-27 15:41:33 +09:00
e39f91e4bf Merge branch 'main' into board-ji 2025-03-27 14:46:25 +09:00
3631c49e43 Merge branch 'main' into board-ji 2025-03-27 14:46:10 +09:00
948199c724 검색 2글자 이상 2025-03-27 14:43:01 +09:00
c3d22b3e4f 검색수정정 2025-03-27 14:39:15 +09:00
d03475bc06 폰드 포멧팅 수정정 2025-03-27 14:34:41 +09:00
f23a7a7e89 영역 수정정 2025-03-27 14:24:57 +09:00
43bcaf1b75 Merge branch 'khj' 2025-03-27 14:08:39 +09:00
a6904fce09 커서 줌줌 2025-03-27 14:08:13 +09:00
yoon
d40646ff76 Merge branch 'main' of http://192.168.0.251:3000/localnet/localhost-front 2025-03-27 13:56:18 +09:00
yoon
913146393f log -> 등록,수정자로 변경 2025-03-27 13:55:52 +09:00
838640e2d4 Merge branch 'khj' 2025-03-27 13:53:47 +09:00
e6242771b0 용어집수정정 2025-03-27 13:53:19 +09:00
fdf439246e 휴가 수정 2025-03-27 13:37:23 +09:00
92b7d5d4ac 첨부파일 변경 2025-03-27 12:07:59 +09:00
c4ff151581 250327 작업 2025-03-27 12:04:07 +09:00
cdb40ed942 삭제된 댓글일떄 수정
All checks were successful
LocalNet_front/pipeline/head This commit looks good
2025-03-27 11:08:40 +09:00
6d428a3bd6 휴가 버튼 크기 조정 2025-03-27 10:43:52 +09:00
b6d6a100a1 버튼 색상 조정 2025-03-27 10:36:39 +09:00
b25d6758de 휴가 -3개이상 사용 x 2025-03-27 10:30:15 +09:00
5b42847028 250327 중간 커밋 2025-03-27 09:39:03 +09:00
ef1f366ce2 Merge branch 'main' into board-ji 2025-03-27 09:36:43 +09:00
9564788ad8 Merge branch 'main' into board-ji 2025-03-27 09:36:28 +09:00
yoon
4ff3aa8fa3 위치
All checks were successful
LocalNet_front/pipeline/head This commit looks good
2025-03-25 18:48:22 +09:00
yoon
4f0bec0df3 위치 확인
All checks were successful
LocalNet_front/pipeline/head This commit looks good
2025-03-25 18:45:30 +09:00
yoon
921e140aa4 datepicker 공통파일 2025-03-25 18:38:45 +09:00
yoon
6a4820628b 혹시몰라 추가 2025-03-25 18:12:26 +09:00
yoon
8328020eb3 오류
All checks were successful
LocalNet_front/pipeline/head This commit looks good
2025-03-25 18:03:48 +09:00
yoon
041a69a173 오류 확인
All checks were successful
LocalNet_front/pipeline/head This commit looks good
2025-03-25 17:56:35 +09:00
yoon
6abb6a532b 프젝 오류
Some checks failed
LocalNet_front/pipeline/head There was a failure building this commit
2025-03-25 17:53:20 +09:00
yoon
ecaf40ced2 Merge branch 'main' into commuters
All checks were successful
LocalNet_front/pipeline/head This commit looks good
2025-03-25 17:44:50 +09:00
yoon
0da0d604e0 프로젝트 참여인원 많은 순 정렬 2025-03-25 17:44:07 +09:00
yoon
a391d677bd 이름 -> 프로젝트명 2025-03-25 17:43:46 +09:00
yoon
cf52ab8a1b 비밀번호 4자리 alert 2025-03-25 17:43:24 +09:00
031c5f35c6 비밀번호 입력 maxlength 8로 변경 2025-03-25 16:14:11 +09:00
c8662e72bf 휴가선물 마이너스일 때 불가능 2025-03-25 16:06:14 +09:00
a79a8bde73 투표종료 가능능 2025-03-25 15:21:12 +09:00
04391cc9d9 Merge branch 'main' into board-ji 2025-03-25 15:04:54 +09:00
86a5a75e9b Merge branch 'main' into board-ji 2025-03-25 15:04:47 +09:00
74bcc71a2a 폰트 2025-03-25 15:04:26 +09:00
bcf8e21ccd Merge branch 'khj' 2025-03-25 15:03:35 +09:00
4abb705a39 카드 크기 조정정 2025-03-25 15:03:09 +09:00
69234678d1 Merge branch '250325_board'
All checks were successful
LocalNet_front/pipeline/head This commit looks good
2025-03-25 12:55:32 +09:00
e393574ae6 댓글 작성 시 줄바꿈 하고 글 쓰면 반영이 안됌 >> 반영되서 댓글 나오게 수정 2025-03-25 12:55:15 +09:00
b7026afcbd 익명 댓글 수정 할 때 삭제 버튼 두 번 눌러야지 input나옴 >> 수정
All checks were successful
LocalNet_front/pipeline/head This commit looks good
2025-03-25 12:43:45 +09:00
30e06407e3 Merge branch '250325_board' 2025-03-25 12:28:10 +09:00
e6e2062474 수정 시 목록으로 이동에서 > 상세로 이동 2025-03-25 12:27:37 +09:00
69f90e6e6e Merge branch '250325_board' 2025-03-25 12:23:40 +09:00
fe9fba3904 비밀번호 maxlength 4 설정 2025-03-25 12:23:13 +09:00
37b77f8052 Merge branch 'main' into board-ji 2025-03-25 11:20:32 +09:00
ca1a466ce2 Merge branch 'main' into board-ji 2025-03-25 11:20:18 +09:00
5082dd2de5 게시판 프로필 커서 안뜨게 2025-03-25 11:19:44 +09:00
46d9801689 Merge branch 'khj' 2025-03-25 11:04:43 +09:00
1dceec000c 수정사항 수정정 2025-03-25 11:04:08 +09:00
128c0f36b9 250325 WORK 2025-03-25 10:52:16 +09:00
33cd40038f 커스텀 버튼 추가 2025-03-25 10:44:09 +09:00
0636f74c9b 이미 사용된 타입휴가 활성화 방지 2025-03-25 10:35:09 +09:00
44de501340 Merge branch 'main' of http://192.168.0.251:3000/localhost/localhost-front 2025-03-24 15:46:05 +09:00
yoon
186e8caa01 Merge branch 'main' into project-list
All checks were successful
LocalNet_front/pipeline/head This commit looks good
2025-03-24 15:41:26 +09:00
yoon
900a92f2d8 Merge branch 'main' of http://192.168.0.251:3000/localnet/localhost-front 2025-03-24 15:40:38 +09:00
yoon
7050709c31 date type input 달력, 데이트피커 추가 2025-03-24 15:40:23 +09:00
52d520f5e4 게시글 에디터 이미지 수정, 제거 로직 추가 2025-03-24 14:01:06 +09:00
yoon
676636b246 back btn 추가 2025-03-24 10:25:49 +09:00
a72eb1f81a Merge branch 'main' of http://192.168.0.251:3000/localhost/localhost-front
All checks were successful
LocalNet_front/pipeline/head This commit looks good
2025-03-24 09:47:35 +09:00
1050d64e80 minlength 원복 2025-03-24 09:47:33 +09:00
yoon
9249610442 프로제트 관련 수정 2025-03-24 09:44:44 +09:00
yoon
aa22023ca3 Merge branch 'main' of http://192.168.0.251:3000/localnet/localhost-front
All checks were successful
LocalNet_front/pipeline/head This commit looks good
2025-03-21 19:56:01 +09:00
yoon
fbb42682f0 용어집 css, 로직변경 2025-03-21 19:55:56 +09:00
58d590bf1a 품수정정 2025-03-21 15:10:32 +09:00
7c5437312b Merge branch 'khj' 2025-03-21 15:05:09 +09:00
93d840cf5a 투표수정정 2025-03-21 15:04:27 +09:00
5f43cc1d19 휴가 아이콘 위치 수정 2025-03-21 14:45:07 +09:00
e6a4399761 . 2025-03-21 14:42:51 +09:00
7181ff1d1b . 2025-03-21 14:42:06 +09:00
923f2683f0 . 2025-03-21 14:11:43 +09:00
1786d03061 체크박스 2025-03-21 14:11:04 +09:00
bbcce3b308 .
All checks were successful
LocalNet_front/pipeline/head This commit looks good
2025-03-21 12:21:29 +09:00
a37d5378aa . 2025-03-21 12:19:26 +09:00
31ccd0c911 데이트 피커 위치조정 2025-03-21 11:20:58 +09:00
be3f8a8b36 댓글작성 수정정
All checks were successful
LocalNet_front/pipeline/head This commit looks good
2025-03-21 10:56:56 +09:00
e8753588ca 익명 닉네임 수정
All checks were successful
LocalNet_front/pipeline/head This commit looks good
2025-03-21 10:46:58 +09:00
57e3489af5 Merge branch 'main' into vacation 2025-03-21 10:16:23 +09:00
8324095bf4 Merge branch 'main' into vacation 2025-03-21 10:16:11 +09:00
724816eb82 게시물 닉네임 수정,회원등록 경고문구 css 수정 2025-03-21 10:14:50 +09:00
93953aedcc 에디터 첨부파일 인덱스 업데이트 로직 추가 2025-03-20 16:12:54 +09:00
8358f60d74 main 2025-03-20 16:10:40 +09:00
ae9c8735b1 커스텀 css 수정 2025-03-20 16:06:11 +09:00
7d70aef38d 폼 눌러도 달력 나오게 2025-03-20 16:02:50 +09:00
f3b7ae315f 날짜 폼만 눌러도 나오게 2025-03-20 15:26:32 +09:00
204b7a28a5 Merge branch 'khj' 2025-03-20 14:56:28 +09:00
946ec28ec8 투표수정정 2025-03-20 14:56:00 +09:00
e6eb91d571 폰트 2025-03-20 14:51:26 +09:00
yoon
5212c4f6d8 카테고리 추가 버튼 css 수정 2025-03-20 14:26:31 +09:00
yoon
aad172e90b Merge branch 'main' into style 2025-03-20 14:06:56 +09:00
yoon
2c81f1c110 new 반짝반짝 2025-03-20 14:06:26 +09:00
658660015e Merge branch 'main' into vacation 2025-03-20 13:42:19 +09:00
c3ef423b16 Merge branch 'main' into vacation 2025-03-20 13:40:40 +09:00
a171736815 폰트 찾아놓음 2025-03-20 13:38:31 +09:00
yoon
61f9fc51fd Merge branch 'main' into style 2025-03-20 13:34:41 +09:00
yoon
32bd8998d4 css 2025-03-20 13:32:24 +09:00
d2560bff1a Merge branch 'main' into board-ji 2025-03-20 13:02:17 +09:00
8d9f297138 Merge branch 'main' into board-ji 2025-03-20 13:02:08 +09:00
b67076a3ca 게시판 익명 닉네임, 경고문구css 수정 2025-03-20 13:00:55 +09:00
yoon
13a80e479a 로그인 오류 관련 alert 수정 2025-03-20 11:00:19 +09:00
733510213a Merge branch 'khj' 2025-03-20 10:53:38 +09:00
ca8ce5e59a 재수정정 2025-03-20 10:53:13 +09:00
0abaea33fd 수정정 2025-03-20 10:52:59 +09:00
bd5b1db86b 수정정 2025-03-20 10:52:19 +09:00
c22bfe1660 Merge branch 'vacation' 2025-03-20 10:25:37 +09:00
e955ac144a Merge branch 'vacation' 2025-03-20 10:25:25 +09:00
5c4a263e7a Merge branch 'main' into vacation 2025-03-20 10:25:02 +09:00
3026550abe Merge branch 'main' into vacation 2025-03-20 10:24:52 +09:00
a50799c021 카테타입추가가 2025-03-20 10:18:39 +09:00
f8cf106a54 my work temp save 2025-03-20 10:17:03 +09:00
c75e8a51ea Merge branch 'khj' 2025-03-20 10:16:38 +09:00
d78fab2143 용어집수정정 2025-03-20 10:16:05 +09:00
f25ad7ffed 카테고리수정정 2025-03-20 10:11:13 +09:00
28067c7f02 휴가 수정 2025-03-20 09:58:25 +09:00
yoon
b374d9ce80 Merge branch 'main' into project-list 2025-03-20 09:56:04 +09:00
yoon
cc567a5f4f 내가 참여하지 않는 그룹이 없는경우 나누는 라인 안보이게 수정 2025-03-20 09:54:11 +09:00
4c01039581 Merge branch 'main' into vacation 2025-03-20 09:47:55 +09:00
184bf4bd2b Merge branch 'main' into vacation 2025-03-20 09:46:50 +09:00
50d3b0d257 휴가 로직 변경 2025-03-19 17:51:28 +09:00
yoon
90d21869b3 프로젝트 참여자 관련 수정 2025-03-19 14:05:27 +09:00
6a8d1ff042 Merge branch 'main' into workMain 2025-03-18 21:34:02 +09:00
b96a24887f 수정정 2025-03-18 20:40:13 +09:00
d466af642c 수정했는데 테스트 plz 2025-03-18 20:11:11 +09:00
yoon
86602d7ffe 비밀번호 재설정 2025-03-18 16:01:46 +09:00
yoon
d4e0728d39 스크롤바 복구 2025-03-18 15:59:31 +09:00
yoon
f1b113f2a6 수정 완료시 바로 반영 2025-03-18 15:58:54 +09:00
yoon
08cd1c5922 select 프로젝트 구분 2025-03-18 15:58:42 +09:00
6d883aacb3 Merge branch 'main' into vacation 2025-03-18 15:52:46 +09:00
yoon
ad0a5653f4 Merge branch 'main' of http://192.168.0.251:3000/localnet/localhost-front 2025-03-18 15:50:42 +09:00
yoon
8c6e54316a 비밀번호 4자리 이상 2025-03-18 15:50:29 +09:00
dfb2a3e57e 필터 날짜 및 페이지 개수 100개 추가 2025-03-18 15:48:45 +09:00
701497a0b0 d
All checks were successful
LocalNet_front/pipeline/head This commit looks good
2025-03-18 14:27:21 +09:00
156cfb0488 1
All checks were successful
LocalNet_front/pipeline/head This commit looks good
2025-03-18 14:24:21 +09:00
2be48fe901 workMainViewPublish 2025-03-18 12:52:12 +09:00
yoon
4c380efec7 캘린더 반응형 css 수정
All checks were successful
LocalNet_front/pipeline/head This commit looks good
2025-03-18 12:50:43 +09:00
6613fed587 게시물이 없을 때
All checks were successful
LocalNet_front/pipeline/head This commit looks good
2025-03-18 12:41:20 +09:00
3d7257338b Merge branch 'main' of http://192.168.0.251:3000/localhost/localhost-front 2025-03-18 12:34:25 +09:00
8c89de1731 게시글 비밀번호 입력란 노출 로직 변경 2025-03-18 12:34:16 +09:00
f2ad06756b 캐치문 수정
All checks were successful
LocalNet_front/pipeline/head This commit looks good
2025-03-18 12:26:52 +09:00
00acd58995 에러페이지 수정 2025-03-18 12:23:23 +09:00
yoon
adbc5d0383 투표 css 수정 2025-03-18 12:17:24 +09:00
yoon
e353309c19 아이콘 pe-none 2025-03-18 12:17:15 +09:00
aaeda4b0cc 라우터 2025-03-18 11:10:49 +09:00
69baec5045 에러페이지 css 수정정 2025-03-18 11:09:11 +09:00
035aa88a26 Merge branch 'main' of http://192.168.0.251:3000/localhost/localhost-front 2025-03-18 11:05:22 +09:00
yoon
402c75320c Merge branch 'main' of http://192.168.0.251:3000/localnet/localhost-front 2025-03-18 11:03:09 +09:00
yoon
bdf2bf41bf toast css 2025-03-18 11:03:06 +09:00
92bdf8592a Merge branch 'main' of http://192.168.0.251:3000/localhost/localhost-front 2025-03-18 10:57:25 +09:00
0ff4724d88 Merge branch 'main' of http://192.168.0.251:3000/localnet/localhost-front 2025-03-18 10:57:15 +09:00
72570e11a2 로그인안하고 경로로 들어갈때 모든 메뉴 막기 2025-03-18 10:57:13 +09:00
00b2fa8c0f Merge branch 'main' of http://192.168.0.251:3000/localhost/localhost-front 2025-03-18 10:56:59 +09:00
29d8095107 댓글 노출 기본 최신순, 수정시 수정됨 문구 표기 2025-03-18 10:56:48 +09:00
b393a29026 수정정 2025-03-18 10:56:12 +09:00
03bb18a1e8 Merge branch 'khj' 2025-03-18 10:50:28 +09:00
b26cf05019 내가한투표수정정 2025-03-18 10:50:00 +09:00
yoon
e79bbf207c 에디터 css 2025-03-18 10:42:33 +09:00
yoon
0e0c4ceed4 필요없는 코드 삭제 2025-03-18 10:35:15 +09:00
aee91c0ce7 Merge branch 'main' of http://192.168.0.251:3000/localnet/localhost-front 2025-03-18 10:30:53 +09:00
6e3ab7acd1 권한부여 css 2025-03-18 10:30:52 +09:00
yoon
250a909ebf Merge branch 'main' of http://192.168.0.251:3000/localnet/localhost-front 2025-03-18 10:30:44 +09:00
yoon
900d1d21a5 Merge branch 'main' into style 2025-03-18 10:30:15 +09:00
1a93a15b44 Merge branch 'main' of http://192.168.0.251:3000/localnet/localhost-front 2025-03-18 10:28:42 +09:00
53061dc5f1 Merge branch 'khj' 2025-03-18 10:28:39 +09:00
yoon
1fcff89176 커스텀 에러 처리 2025-03-18 10:28:30 +09:00
cdb08511b7 투표 오류수정정 2025-03-18 10:28:09 +09:00
201be293f8 탑바 간격조정 2025-03-18 10:20:20 +09:00
c730ea6cd3 댓글 업데이트 로직 변경
All checks were successful
LocalNet_front/pipeline/head This commit looks good
2025-03-17 22:47:57 +09:00
yoon
942f2c660c Merge branch 'main' of http://192.168.0.251:3000/localnet/localhost-front 2025-03-17 16:04:22 +09:00
yoon
2cc0db21ac btn css 수정 2025-03-17 16:04:19 +09:00
8feaf568a9 Merge branch 'khj' 2025-03-17 15:59:49 +09:00
145c188d19 투표 본인 ㄴ비활성화 막음음 2025-03-17 15:59:20 +09:00
yoon
0d05666532 board css 수정 2025-03-17 15:45:28 +09:00
yoon
0a514fa3a0 css 수정 2025-03-17 15:21:11 +09:00
yoon
0c32f26701 에러 해결 2025-03-17 15:19:11 +09:00
cc8e347aa9 로딩 수정 2025-03-17 15:10:07 +09:00
ce1011b96f . 2025-03-17 15:00:56 +09:00
3ee5565df6 휴가버튼수정 2025-03-17 14:59:32 +09:00
69f928d37f Merge branch 'khj' 2025-03-17 14:25:56 +09:00
eb20522969 투표인ㄷ원체크 수정정 2025-03-17 14:25:27 +09:00
d2137eed02 휴가 2025-03-17 14:17:02 +09:00
yoon
1abd8f02d8 투표 css 2025-03-17 14:09:24 +09:00
aae725398c 등록 수정정 2025-03-17 14:00:37 +09:00
69887a798f Merge branch 'khj' 2025-03-17 13:56:47 +09:00
4962ff4c92 용어어등록수정 2025-03-17 13:55:59 +09:00
yoon
99e4462ea8 container -> container-xxl 수정 2025-03-17 13:29:15 +09:00
626c8a97cb 익명 댓글과 회원 댓글 디폴트 이미지 구분
All checks were successful
LocalNet_front/pipeline/head This commit looks good
2025-03-17 13:26:51 +09:00
yoon
1b83422555 Merge branch 'main' of http://192.168.0.251:3000/localnet/localhost-front 2025-03-17 13:22:53 +09:00
yoon
051bdcf0a0 style 삭제 2025-03-17 13:22:50 +09:00
90dc01b98d 게시글 답변 css 공통으로 변경 2025-03-17 13:19:24 +09:00
de9c8d5638 Merge branch 'khj' 2025-03-17 13:08:14 +09:00
55d9ea0ebe 버튼 수정 2025-03-17 13:07:44 +09:00
yoon
2290a2b5af Merge branch 'main' of http://192.168.0.251:3000/localnet/localhost-front
All checks were successful
LocalNet_front/pipeline/head This commit looks good
2025-03-17 12:00:44 +09:00
yoon
f7abeba675 개인참여기간 2025-03-17 12:00:26 +09:00
8c9c7f147c 댓글 수정시 게시글 수정 비밀번호 입력창 노출 안되게 수정 2025-03-17 11:24:36 +09:00
58100f848c . 2025-03-17 11:14:42 +09:00
6f937d3800 게시판 수정페이지 수정 2025-03-17 11:07:24 +09:00
yoon
4d5a347459 scroll-top-btn 위치변경 2025-03-17 10:50:53 +09:00
yoon
5d4c90e604 Merge branch 'main' of http://192.168.0.251:3000/localnet/localhost-front 2025-03-17 10:44:55 +09:00
yoon
4bd8ea10af 챗 사이드바, nav 조정 2025-03-17 10:44:52 +09:00
65a20845d7 함수 수정 2025-03-17 10:36:58 +09:00
a3b17c0215 Merge branch 'main' of http://192.168.0.251:3000/localhost/localhost-front 2025-03-17 10:29:04 +09:00
60069ecaae 게시판 수정 페이지 비밀번호 2025-03-17 10:29:02 +09:00
7a56ddfaa0 Merge branch 'khj' 2025-03-17 10:25:21 +09:00
4258881d4b 용어집수정정 2025-03-17 10:24:41 +09:00
5c39cb3dad Merge branch 'board_comment5' 2025-03-17 09:21:25 +09:00
277524c0c3 수정화면 비밀번호 확인 로직 추가 2025-03-17 09:13:25 +09:00
985351c8f0 오류페이지 noLayout 설정 추가 2025-03-15 00:56:52 +09:00
74be397b5d 123 2025-03-14 16:16:37 +09:00
34bef477f9 ㄱㄱ 2025-03-14 16:14:10 +09:00
3900e2cff4 에러메시지지 2025-03-14 16:06:27 +09:00
c51da7f56d 코드수정정 2025-03-14 15:37:42 +09:00
9e4207de95 Merge branch 'main' of http://192.168.0.251:3000/localnet/localhost-front 2025-03-14 15:27:43 +09:00
d5c54d8195 투표 수정 2025-03-14 15:26:50 +09:00
yoon
32268be1c6 Mobile 2025-03-14 15:15:06 +09:00
05c92bdf55 Merge branch 'main' into vacation 2025-03-14 14:32:36 +09:00
2166bc66a9 Merge branch 'main' into vacation 2025-03-14 14:32:21 +09:00
8f57e2a1eb 권한페이지 링크설정정 2025-03-14 14:31:51 +09:00
236928f1da 권한부여 2025-03-14 13:58:15 +09:00
fe8abc7f7c 용어집 수정정 2025-03-14 13:16:47 +09:00
24b75776df Merge branch 'board_comment5' 2025-03-14 10:53:04 +09:00
f761e3e15e 비밀번호 경고문 input 아래에 2025-03-14 10:52:30 +09:00
yoon
c0ea8469db 맨위로 가는 버튼 2025-03-14 10:48:22 +09:00
89b5a330d7 휴가,게시판에디터 수정 2025-03-14 10:23:50 +09:00
yoon
bd58503912 스크롤 안보이게 2025-03-14 10:06:57 +09:00
yoon
e949e4a0a8 Merge branch 'main' into commuters 2025-03-14 10:04:00 +09:00
yoon
1eba161060 출퇴근 2025-03-14 10:03:34 +09:00
84f5cf4412 댓글 수정시 공백체크 2025-03-14 01:53:35 +09:00
yoon
2ec81f274d 날짜 min 수정 2025-03-13 21:45:18 +09:00
yoon
946e3441e3 Merge branch 'main' into login 2025-03-13 21:18:18 +09:00
yoon
2a21c12017 전화번호 2025-03-13 21:15:39 +09:00
8dd206d32d console.log 삭제 2025-03-13 19:07:02 +09:00
09a665d079 front public 이미지 경로 변경
All checks were successful
LocalNet_front/pipeline/head This commit looks good
2025-03-13 19:03:43 +09:00
dabeac68c1 익명일 경우 익명 이미지로 2025-03-13 18:57:11 +09:00
ec2f7cae09 휴가 수정정 2025-03-13 16:16:55 +09:00
yoon
f100706a83 현재위치
All checks were successful
LocalNet_front/pipeline/head This commit looks good
2025-03-13 12:28:44 +09:00
yoon
062742c602 Merge branch 'main' into commuters
All checks were successful
LocalNet_front/pipeline/head This commit looks good
2025-03-13 12:15:29 +09:00
yoon
8ef12c66d6 출퇴근 2025-03-13 12:14:44 +09:00
e9f7b5d358 Merge branch 'khj' 2025-03-13 12:09:09 +09:00
d38f89c5bc 휴가수정2 2025-03-13 12:08:39 +09:00
5f6a3c0fcf 투표cssㅜㅅ정정 2025-03-13 12:08:24 +09:00
f369712a81 휴가수정 2025-03-13 12:06:18 +09:00
be2608a112 비밀번호 자동입력 제외 추가, 버튼 위치 조정 2025-03-13 11:26:19 +09:00
yoon
495c714b80 Merge branch 'main' into commuters 2025-03-13 11:15:08 +09:00
yoon
a772a2b4e6 출퇴근 2025-03-13 11:14:38 +09:00
979321d533 . 2025-03-13 11:10:26 +09:00
a73ace7ac3 Merge branch 'main' of http://192.168.0.251:3000/localnet/localhost-front 2025-03-13 11:04:31 +09:00
45c1ddf17c 게시판 검색바 위치 수정 2025-03-13 11:04:29 +09:00
ff6a032cd6 Merge branch 'boardview' 2025-03-13 11:03:06 +09:00
5cb6f4ae38 인증 필요시 로그인 페이지 이동, 공통함수 설명 추가, 댓글 이미지 경로 변경(공통함수로), alert 을 토스트로 변경 2025-03-13 11:02:41 +09:00
2c1566f49e 게시판 리스트 css 수정 2025-03-13 10:59:35 +09:00
2661929cb0 . 2025-03-13 10:33:21 +09:00
4a5ab6238c 게시판 에디터 사진넘치는거 수정 2025-03-13 10:21:20 +09:00
yoon
10aaae307e Merge branch 'main' into commuters 2025-03-12 23:02:09 +09:00
yoon
ccad3596c7 출퇴근 2025-03-12 23:00:52 +09:00
fabb1c2c3f 수정 ,삭제시 비밀번호 폼에 공백 방지지 2025-03-11 16:21:20 +09:00
52c211bf08 Merge branch 'boardview' 2025-03-11 16:14:22 +09:00
5b84ef9350 삭제시 댓글 비밀번호 입력창 비활성화 2025-03-11 16:13:53 +09:00
44e72c9532 검색 바 포커스 아웃할때 검색되는거 수정 2025-03-11 16:11:58 +09:00
ff663c0c39 휴가관리 2025-03-11 16:05:58 +09:00
69fb0669be 휴가 수정완
Some checks failed
LocalNet_front/pipeline/head There was a failure building this commit
2025-03-11 16:00:12 +09:00
a3ea106265 Merge branch 'boardmodify3' 2025-03-11 15:56:55 +09:00
1454719bae 1 2025-03-11 15:56:40 +09:00
d2bc6f4272 캘린더 높이 오토 2025-03-11 15:29:52 +09:00
3fa7eff7d9 휴가 저장 2025-03-11 15:17:16 +09:00
9c47467ba7 Merge branch 'boardmodify3' 2025-03-11 15:03:50 +09:00
757370cf91 다운로드 드랍다운 메뉴 우측에 안가리게 z-index 수정 2025-03-11 15:03:30 +09:00
c8418ba292 Merge branch 'boardmodify3' 2025-03-11 14:50:29 +09:00
15bfd59c59 대댓글 폼 여는 버튼 한번 더 클릭시 닫히는데 그때 x버튼으로 바뀌게 2025-03-11 14:50:11 +09:00
ac8daab212 휴가 로그삭제제
All checks were successful
LocalNet_front/pipeline/head This commit looks good
2025-03-11 14:36:56 +09:00
c9271aebfc 휴가 수정 2025-03-11 14:35:45 +09:00
936be84a6f Merge branch 'khj' 2025-03-11 14:27:01 +09:00
058f0c00fa 투표수정정 2025-03-11 14:26:30 +09:00
72f3622216 Merge branch 'boardmodify3' 2025-03-11 13:59:39 +09:00
538413c963 익명일때 수정,삭제 비밀번호 폼 한번 더 클릭시 폼 닫히게 2025-03-11 13:59:21 +09:00
10c429f9f1 휴가 기능오류수정 2025-03-11 13:57:36 +09:00
6e161fc2b3 Merge branch 'boardmodify3' 2025-03-11 13:35:03 +09:00
ef1101d9ee 수정 토글 닫기도 되게 수정 2025-03-11 13:33:06 +09:00
bd3d689ddd Merge branch 'boardmodify3' 2025-03-11 12:51:40 +09:00
003e9da85e console 제거 2025-03-11 12:51:21 +09:00
5778a5dd4a Merge branch 'boardmodify3' 2025-03-11 12:38:18 +09:00
fb8c00fd6b 게시글 프로필 이미지 보이게 익명일경우도 익명이미지 노출하도록 2025-03-11 12:38:03 +09:00
e04d1c65fe 휴가css 수정 2025-03-11 11:09:26 +09:00
408c72d3c0 Merge branch 'boardmodify3' 2025-03-11 11:03:33 +09:00
c3dabc4262 비밀번호 alert 노출위치 변경 2025-03-11 11:03:17 +09:00
5d2fd80098 Merge branch 'style' 2025-03-11 10:49:54 +09:00
8135926150 Merge branch 'style' 2025-03-11 10:48:38 +09:00
e33d2b870d Merge branch 'main' into style 2025-03-11 10:48:10 +09:00
ff3bbebfd5 Merge branch 'main' into style 2025-03-11 10:47:53 +09:00
19fad56ae6 Merge branch 'boardmodify3' 2025-03-11 10:42:54 +09:00
7044526284 게시글 삭제 재수정 2025-03-11 10:42:37 +09:00
61935cfb68 Merge branch 'boardmodify3' 2025-03-11 10:38:47 +09:00
93f6d1f3b9 게시글 삭제 수정 2025-03-11 10:38:12 +09:00
c5013e0ef0 Merge branch 'main' into board-ji 2025-03-11 10:35:05 +09:00
ccc195435c Merge branch 'main' into board-ji 2025-03-11 10:34:51 +09:00
7ec6424667 댓글 삭제수정 2025-03-11 10:34:10 +09:00
23f39170da 버튼수정정 2025-03-11 10:29:42 +09:00
0aad1516b7 Merge branch 'main' of http://192.168.0.251:3000/localnet/localhost-front 2025-03-11 10:21:43 +09:00
6d1905ba03 버튼변경경 2025-03-11 10:21:11 +09:00
yoon
65e620d579 Merge branch 'main' into commuters 2025-03-11 09:24:00 +09:00
yoon
400eadf3d9 휴가관리 aside style 수정 2025-03-10 21:58:23 +09:00
159dd41e4a Merge branch 'boardmodify3'
All checks were successful
LocalNet_front/pipeline/head This commit looks good
2025-03-10 16:25:03 +09:00
c90220785c 수정시 공백체크, 수정 시 toast 2025-03-10 16:24:25 +09:00
8c2bbe188b Merge branch 'khj' 2025-03-10 16:19:29 +09:00
f70096e975 카테고리 버튼 초기화 추가가 2025-03-10 16:18:24 +09:00
yoon
eca45a84c4 Merge branch 'main' into project-list 2025-03-10 15:45:01 +09:00
yoon
2f1464bf6b 날짜 min 2025-03-10 15:44:15 +09:00
c30ccea258 휴가 수정 2025-03-10 15:38:12 +09:00
915e2476e3 용어집 수정정 2025-03-10 15:18:07 +09:00
2aa31ab64f 수정 아이콘 수정정 2025-03-10 15:10:48 +09:00
d7cc2c056a Merge branch 'khj' 2025-03-10 15:05:07 +09:00
9e48e2bc03 수정정 2025-03-10 15:04:46 +09:00
yoon
c2bac1c3fb 전화번호 숫자만, 주소 디세이블, 컬러 변경 시 alert 2025-03-10 14:57:05 +09:00
yoon
1384ae571d 출근 2025-03-10 14:25:07 +09:00
aac2e21c08 Merge branch 'main' of http://192.168.0.251:3000/localnet/localhost-front 2025-03-10 14:24:49 +09:00
df024efdd3 댓글, 비밀번호 입력 감지 경고문구 제거 2025-03-10 13:02:09 +09:00
96b9e96d7a Merge branch 'khj' 2025-03-10 13:00:53 +09:00
16b95d546c Merge branch 'main' of http://192.168.0.251:3000/localnet/localhost-front 2025-03-10 13:00:38 +09:00
1e50403e57 튜표 수정정 2025-03-10 13:00:15 +09:00
e30c58c343 font 수정정 2025-03-10 13:00:00 +09:00
42bb7c4d55 Merge branch 'main' into vacation 2025-03-10 12:53:16 +09:00
129cc04f53 Merge branch 'main' into vacation 2025-03-10 12:53:01 +09:00
d5a62f832c 휴가 수정 2025-03-10 12:52:23 +09:00
dc4ae608eb 1.대댓글 슬롯형태로 변경 2.대댓글익명클릭시해당대댓글에비밀번호입력이나오게수정
All checks were successful
LocalNet_front/pipeline/head This commit looks good
2025-03-10 11:22:45 +09:00
3ac834dd4a 휴가 여려년도 삭제 수정 2025-03-10 10:21:23 +09:00
9c01a84749 Merge branch 'boardmodify3' 2025-03-10 10:09:30 +09:00
a76bfbcc25 익명이아닐경우비밀번호창안나오게&댓글maxlength설정 2025-03-10 10:08:40 +09:00
f7e11a5466 로딩삭제제 2025-03-07 16:24:35 +09:00
e9c0b09d93 Merge branch 'khj' 2025-03-07 16:21:31 +09:00
f2c231288c Merge branch 'main' of http://192.168.0.251:3000/localnet/localhost-front 2025-03-07 16:20:27 +09:00
2bb292c48d 수정 투표표 2025-03-07 16:20:06 +09:00
bdcb75fc58 휴가css 수정정 2025-03-07 14:45:24 +09:00
dae3ef8ede Merge branch 'main' of http://192.168.0.251:3000/localhost/localhost-front 2025-03-07 14:22:40 +09:00
a4ee364aad Merge branch 'main' of http://192.168.0.251:3000/localhost/localhost-front 2025-03-07 14:22:31 +09:00
e340319595 휴가 css 수정 2025-03-07 14:19:12 +09:00
627e29ec7a 인증 관련 토스트 제거 및 브라우저 비밀번호 저장안뜨게 2025-03-07 14:16:57 +09:00
yoon
b08e72b813 console 삭제 2025-03-07 14:08:26 +09:00
144738028d 로딩 이미지 수정 2025-03-07 14:05:34 +09:00
9698a9d441 커스텀 수정 2025-03-07 13:45:46 +09:00
dd855634a0 로딩 2025-03-07 13:37:54 +09:00
4641800676 수정정 2025-03-07 13:19:19 +09:00
39dd213949 Merge branch 'main' into loading 2025-03-07 13:13:04 +09:00
b222e606c2 Merge branch 'main' into loading 2025-03-07 13:12:52 +09:00
2cc3166756 Merge branch 'main' of http://192.168.0.251:3000/localnet/localhost-front 2025-03-07 13:11:18 +09:00
0d016ca2e1 휴가관리 로딩페이지 추가 2025-03-07 12:51:33 +09:00
2af99d9b51 Merge branch 'main' of http://192.168.0.251:3000/localhost/localhost-front 2025-03-07 12:44:24 +09:00
925f6b2fbc Merge branch 'vacation-css' 2025-03-07 12:43:08 +09:00
2d29bc56ec 로딩페이지(게시판 상세,투표리스트) 2025-03-07 12:40:43 +09:00
fc2f7bd4cb 커스텀 css수정 2025-03-07 12:18:40 +09:00
02b3c8dd41 Merge branch 'main' of http://192.168.0.251:3000/localnet/localhost-front 2025-03-07 12:16:04 +09:00
a2f2199587 수정 2025-03-07 12:15:41 +09:00
abfc872d04 탑바 css 수정 2025-03-07 12:14:54 +09:00
c733197336 Merge branch 'khj' 2025-03-07 12:13:07 +09:00
a64cab08eb Merge branch 'main' into vacation-css 2025-03-07 12:12:58 +09:00
564e53b6f2 Merge branch 'main' into vacation-css 2025-03-07 12:12:45 +09:00
114b56ddc7 Merge branch 'main' of http://192.168.0.251:3000/localnet/localhost-front 2025-03-07 12:12:36 +09:00
yoon
3fe47a83bf Merge branch 'main' into project-list 2025-03-07 12:10:08 +09:00
yoon
cbc3c9dda5 종료된 프로젝트 관련 2025-03-07 12:09:37 +09:00
73b831a13f Merge branch 'main' of http://192.168.0.251:3000/localnet/localhost-front 2025-03-07 12:09:36 +09:00
f7d688bb60 Merge branch 'main' into vacation-css 2025-03-07 11:21:19 +09:00
6e00c338ea 휴가 css 수정완 2025-03-07 11:20:27 +09:00
7a51ee37c8 수정정 2025-03-07 11:18:58 +09:00
yoon
292b479523 종료 프로젝트 배경 2025-03-07 11:15:03 +09:00
a1de83b5cc 용어집 수정정 2025-03-07 11:12:56 +09:00
ad6ed9df55 Merge branch 'boardmodify'
All checks were successful
LocalNet_front/pipeline/head This commit looks good
123
2025-03-07 10:24:47 +09:00
243b970227 게시글 수정 화면 작업 2025-03-07 10:22:22 +09:00
21fb66bafe 화면 반응형으로 수정 2025-03-06 16:23:29 +09:00
8636bd1ab9 탑바 고정 2025-03-06 16:19:11 +09:00
63dacb4c3c Merge branch 'khj' 2025-03-06 15:29:59 +09:00
c3b1331128 Merge branch 'main' of http://192.168.0.251:3000/localnet/localhost-front 2025-03-06 15:29:39 +09:00
e5f821e15a 용어집 수정정 2025-03-06 15:29:19 +09:00
19ce33e503 수정 2025-03-06 15:09:36 +09:00
85f197b001 .. 2025-03-06 15:09:09 +09:00
04bcbcb781 . 2025-03-06 15:03:26 +09:00
6904902755 휴가 css 수정 2025-03-06 15:02:15 +09:00
yoon
17fb20ae1e Merge branch 'main' into login 2025-03-06 13:16:47 +09:00
yoon
2331a345aa 회원가입 시 색상 체크 2025-03-06 13:16:07 +09:00
yoon
d11e9df73c estj 2025-03-06 13:15:51 +09:00
d74c0ddacb 버튼 간격 수정 및 비밀번호입력창 추가 2025-03-06 12:21:50 +09:00
cd1c707b12 휴가관리 수정 2025-03-06 12:17:52 +09:00
6ac3d587f8 아이콘 간격 수정 2025-03-06 12:15:37 +09:00
6c4fc7ac8e 라이트 모드, 다크모드 아이콘 주석처리 2025-03-06 10:14:19 +09:00
708309f6a0 Merge branch 'main' of http://192.168.0.251:3000/localnet/localhost-front 2025-03-06 09:50:56 +09:00
5ae3d3bc75 게시판 삭제된 댓글 안보이게 2025-03-06 09:47:37 +09:00
0cfeb2fc1d 게시판 댓글삭제제 2025-03-04 17:42:46 +09:00
4dc807c452 Merge remote-tracking branch 'origin/main' into board-file 2025-03-04 17:01:22 +09:00
27c9392d68 게시판 수정중 2025-03-04 16:37:49 +09:00
fbd345403a 오타 수정
All checks were successful
LocalNet_front/pipeline/head This commit looks good
2025-03-04 13:53:25 +09:00
3ba4718e0a 메뉴바 pc 고정 및 토글버튼 제거 2025-03-04 13:52:25 +09:00
af63fd12b6 오타 수정 2025-03-04 13:24:24 +09:00
58647b225c 젠킨스파일수정
All checks were successful
LocalNet_front/pipeline/head This commit looks good
2025-03-04 12:51:32 +09:00
8f79ed6680 휴가리스트 댓글개수 2025-03-04 10:42:39 +09:00
dae4cd2edd dev 포트정보 변경
All checks were successful
LocalNet_front/pipeline/head This commit looks good
2025-03-04 10:00:10 +09:00
b4c9169807 수정
All checks were successful
LocalNet_front/pipeline/head This commit looks good
2025-03-01 13:44:33 +09:00
1c58976afa build 설정 "prod" 에서 "dev" 로 수정
Some checks failed
LocalNet_front/pipeline/head There was a failure building this commit
2025-03-01 13:42:40 +09:00
607b954339 젠킨스 설정 수정 2025-03-01 13:41:15 +09:00
330a44314e 이름변경
Some checks failed
LocalNet_front/pipeline/head There was a failure building this commit
2025-03-01 12:05:12 +09:00
027c996626 배포용 젠킨스파일 생성
Some checks failed
LocalNet_front/pipeline/head There was a failure building this commit
2025-03-01 11:47:43 +09:00
b699595239 Merge branch 'main' into board-comment-2 2025-02-28 14:58:32 +09:00
497d45e6df Merge branch 'main' into board-comment-2 2025-02-28 14:58:20 +09:00
cf00134aa5 Merge branch 'main' into wordDict 2025-02-28 14:53:28 +09:00
6df2f00306 Merge branch 'main' into wordDict 2025-02-28 14:53:16 +09:00
42c6a32c3f Merge remote-tracking branch 'origin/main' into board-comment-2 2025-02-28 14:45:18 +09:00
8bc2152490 콘솔 삭제 2025-02-28 14:44:30 +09:00
e8c35bba82 게시판 글쓰기 다시수정 2025-02-28 14:43:19 +09:00
40584f0269 isDeleted 삭제후 메세지 남기기 하는중 2025-02-28 14:43:18 +09:00
Dang
64a915f0d8 용어집 수정 2025-02-28 14:40:57 +09:00
f5911247fb Merge branch 'board-comment-2' of http://192.168.0.251:3000/localnet/localhost-front into board-comment-2 2025-02-28 14:31:41 +09:00
e63f9498bd isDeleted 수정중 2025-02-28 14:31:36 +09:00
2a55574646 대댓글 익명일때 수정 2025-02-28 14:30:19 +09:00
86a6e5b27b 수정버튼 누르면 초기화 2025-02-28 14:12:33 +09:00
yoon
6526e80879 색상 미리보기 추가 2025-02-28 14:09:27 +09:00
yoon
469688b2d0 지도 휠 안돼는거 수정 2025-02-28 14:09:13 +09:00
c68e8839b3 Merge remote-tracking branch 'origin/main' into board-comment-2 2025-02-28 14:07:19 +09:00
yoon
00a6e9f2e9 Merge branch 'main' into project-list 2025-02-28 14:06:10 +09:00
yoon
ea7735839f @input 추가 2025-02-28 14:05:27 +09:00
64cb7e30c1 대댓글 내용없을때 입력 안되게 2025-02-28 13:59:49 +09:00
af3316fc60 Merge branch 'board-write' 2025-02-28 13:58:33 +09:00
f9289108c6 Merge branch 'board-write' 2025-02-28 13:58:19 +09:00
2d47d1926a Merge branch 'main' into board-write 2025-02-28 13:57:42 +09:00
cdcc1cc552 Merge branch 'main' into board-write 2025-02-28 13:57:15 +09:00
6bff0e9ede 글쓰기 공백처리리 2025-02-28 13:56:51 +09:00
yoon
a9d4f67563 Merge branch 'main' into login 2025-02-28 13:40:16 +09:00
yoon
6a7ed19d54 Merge branch 'main' into login 2025-02-28 13:39:48 +09:00
yoon
3eaafa606d color 색 미리보기 추가, mbti 이미지 추가 2025-02-28 13:38:53 +09:00
yoon
0e3281953d MBTI이미지 추가 2025-02-28 13:22:42 +09:00
08dabf4191 Merge remote-tracking branch 'origin/main' into board-comment-2 2025-02-28 13:19:27 +09:00
613d9f4c16 이미 수정중이면 비밀번호 창 no 2025-02-28 13:17:12 +09:00
ac71a9aa1f 휴가관리 수정 2025-02-28 13:03:52 +09:00
421ec4eaec 익명 댓글 비밀번호 나오는곳 수정해야함 2025-02-28 13:02:55 +09:00
785df5c51f 비밀번호 입력창 초기화 2025-02-28 12:18:09 +09:00
3678b40e1c 댓글 수정중 2025-02-28 12:01:59 +09:00
4720e61958 warning수정 2025-02-28 11:09:10 +09:00
63f8f167eb 경고 수정중 2025-02-28 10:57:25 +09:00
yoon
3e0987657c Merge branch 'main' into login 2025-02-28 10:36:24 +09:00
yoon
e07593c13b 수정 리로드 삭제 2025-02-28 10:35:43 +09:00
yoon
a1773500bf Merge branch 'main' into project-list 2025-02-28 10:23:57 +09:00
yoon
8d14b0b214 수정시 바뀐거 없을 때 막기 2025-02-28 10:13:40 +09:00
yoon
86f85cce10 활성화 된 유저 앞으로 정렬 2025-02-28 10:13:23 +09:00
2cc280717d . 2025-02-28 10:09:45 +09:00
d1c91b30df 주석 삭제 2025-02-28 10:01:53 +09:00
0859ad72d0 메뉴 js 원복 2025-02-28 09:27:34 +09:00
d954a1b68c 댓글 수정 2025-02-28 00:58:38 +09:00
0968a616b6 익명 댓글 수정 완료 2025-02-27 16:46:49 +09:00
e30dd8f7b0 익명 댓글 수정 수정중 2025-02-27 16:35:20 +09:00
b02d99e4b1 댓글 익명 체크 수정 완료 2025-02-27 16:23:45 +09:00
3f81b57142 탑바 프로필 이미지 url 수정 2025-02-27 16:21:51 +09:00
495fe5850a Merge branch 'khj' 2025-02-27 15:45:18 +09:00
1063d82a29 Merge branch 'main' of http://192.168.0.251:3000/localnet/localhost-front 2025-02-27 15:44:57 +09:00
896ca99bdc randomList emit 오류류제거거 2025-02-27 15:44:30 +09:00
yoon
8b8c9f86fd 비밀번호 체크 수정 2025-02-27 14:57:36 +09:00
ffabb284fa Merge branch 'vacation' 2025-02-27 14:51:58 +09:00
07740d0a96 Merge branch 'main' into board-comment-2 2025-02-27 14:51:19 +09:00
f248141108 Merge branch 'main' of http://192.168.0.251:3000/localnet/localhost-front 2025-02-27 14:48:55 +09:00
6942a9e063 디폴트 이미지 추가 2025-02-27 14:48:23 +09:00
Dang
3a78dc6355 용어집 수정 2025-02-27 14:21:19 +09:00
00f632e607 콘솔삭제 2025-02-27 13:59:20 +09:00
0c67d70233 휴가,css 2025-02-27 13:38:51 +09:00
94a270caf8 날짜 포멧팅 함수 추가 2025-02-27 13:37:04 +09:00
yoon
f11de2f2ab Merge branch 'main' into project-list 2025-02-27 13:30:42 +09:00
yoon
581709430f 수정, 등록 나누고 삭제 추가 2025-02-27 13:28:52 +09:00
2eea7d3f53 Merge remote-tracking branch 'origin/wordDict' 2025-02-27 13:23:29 +09:00
beb05db225 Merge branch 'khj' 2025-02-27 13:08:34 +09:00
722df90b4d Merge branch 'main' of http://192.168.0.251:3000/localnet/localhost-front 2025-02-27 13:08:21 +09:00
7f71d5b589 투표수정정 2025-02-27 13:06:35 +09:00
92904c5001 Merge branch 'vacation' 2025-02-27 13:06:29 +09:00
80e59e4355 vacation 수정, 게시판 글쓰기 수정 2025-02-27 13:05:10 +09:00
4145fdc445 .. 2025-02-27 12:45:43 +09:00
9293bba077 비밀번호 확인부터 2025-02-27 12:41:31 +09:00
4e75c988de 로컬, 개발 실행설정 변경 2025-02-27 11:37:51 +09:00
Dang
3d41dc7730 용어집 수정 2025-02-27 11:23:56 +09:00
yoon
0c1e7bc5dd 예시 값 삭제 2025-02-27 11:07:19 +09:00
yoon
b00824bddc 첫번째 입력 값 공백 X 2025-02-27 11:07:03 +09:00
yoon
0e778df967 css 삭제 2025-02-27 11:06:47 +09:00
yoon
bbaf744b45 프로젝트 store 생성 2025-02-27 11:06:25 +09:00
476437f2e6 . 2025-02-27 10:41:53 +09:00
90c90b7236 . 2025-02-27 10:40:53 +09:00
bb1715c71b 휴가 수정정 2025-02-27 10:33:48 +09:00
088f3feede 공통 버튼으로 수정 2025-02-27 10:29:57 +09:00
7ef2ef646b 프로필 부분 이미지 삭제 2025-02-27 09:56:22 +09:00
ed02d33099 검색창 엔터 2025-02-27 09:54:29 +09:00
22741cb803 검색 결과 없을시 2025-02-27 09:51:06 +09:00
98aa44f497 Merge branch 'main' of http://192.168.0.251:3000/localnet/localhost-front 2025-02-27 09:48:33 +09:00
yoon
a7c27a19e4 update:alert 추가 2025-02-27 09:17:27 +09:00
yoon
7e484c12be 수정사항 완료 2025-02-26 19:09:04 +09:00
bd91e0a72d 보드 원복복 2025-02-25 16:27:22 +09:00
yoon
37c335fc12 로그인 시 메인페이지로 가지는데 화면은 안바뀌는거 해결 2025-02-25 16:00:45 +09:00
8e0700a659 min required 속성 추가가 2025-02-25 15:56:52 +09:00
yoon
9087d605e2 Merge branch 'main' into commuters 2025-02-25 15:52:57 +09:00
56757155ee Merge branch 'main' of http://192.168.0.251:3000/localnet/localhost-front 2025-02-25 15:26:30 +09:00
47ab2b6054 투표수정정 2025-02-25 15:25:56 +09:00
yoon
8b34a6b5d3 Merge branch 'main' of http://192.168.0.251:3000/localnet/localhost-front 2025-02-25 14:38:51 +09:00
yoon
9cd4b35ced 인터셉터 변경 2025-02-25 14:38:47 +09:00
9984800c6b 머지충돌해결,댓글대댓글 익명저장 2025-02-25 14:27:15 +09:00
yoon
6e6c949487 Merge branch 'main' into commuters 2025-02-25 14:22:33 +09:00
f4f5ac45f6 Merge branch 'khj' 2025-02-25 14:21:23 +09:00
yoon
56fc9615de Merge branch 'main' into commuters 2025-02-25 14:21:05 +09:00
2292e24bf5 중복수정정 2025-02-25 14:20:55 +09:00
yoon
008fd6c9a9 Merge branch 'main' into wordDict 2025-02-25 14:20:22 +09:00
yoon
6ecf2d5aa1 미완성 2025-02-25 14:18:56 +09:00
yoon
8ffb483aa1 파일 명 변경 2025-02-25 14:18:48 +09:00
yoon
fc443dacdb 프로젝트 store로 관리 2025-02-25 14:18:20 +09:00
b2eafbb9aa Merge branch 'board-comment' 2025-02-25 14:12:56 +09:00
f71651729e 휴가css 수정정 2025-02-25 14:09:31 +09:00
Dang
72c2865b0b 용어집 수정 2025-02-25 14:08:33 +09:00
Dang
fc59cc4e5a Merge remote-tracking branch 'origin/main' into wordDict 2025-02-25 12:35:11 +09:00
a08607e0b1 Merge branch 'vacation' 2025-02-25 12:30:32 +09:00
b0735cfec0 휴가관리 수정 및 공통포맷분리 2025-02-25 12:29:35 +09:00
a2da67b043 복구 2025-02-25 10:13:29 +09:00
99d89008ed 댓글과 대댓글 익명으로 저장장 2025-02-25 10:04:41 +09:00
yoon
d4682bc362 프로필 이미지 높이 설정 변경 2025-02-24 15:44:47 +09:00
yoon
68d96fa098 파일 명 변경 2025-02-24 15:44:01 +09:00
efbeee855a 휴가관리 수정정 2025-02-24 15:29:32 +09:00
84c43d5baf 공통 날짜 포멧팅 함수 추가 2025-02-24 15:27:45 +09:00
yoon
55e5ebc2b3 map 2025-02-24 15:26:45 +09:00
5f80998da8 익명 확인중 2025-02-24 15:26:39 +09:00
yoon
10692e9ddd 엔터 2025-02-24 15:25:17 +09:00
170e472815 익명부분 수정중 2025-02-24 15:11:37 +09:00
yoon
76527da74f Merge branch 'main' into project-list 2025-02-24 14:56:45 +09:00
yoon
99d95cf5a1 주소 지도 추가 2025-02-24 14:55:55 +09:00
yoon
cd5d525575 프로젝트 목록 지도 관련 css 추가 2025-02-24 14:55:36 +09:00
yoon
efa23d871f 카카오 맵 사용 2025-02-24 14:55:16 +09:00
yoon
b4bde43133 kakao-maps 라이브러리 추가 2025-02-24 14:54:58 +09:00
yoon
447d380753 kakao map api key 추가 2025-02-24 14:54:41 +09:00
2ba5121c85 Merge branch 'board-comment' of http://192.168.0.251:3000/localnet/localhost-front into board-comment 2025-02-24 14:04:21 +09:00
a935bcf4d5 로그인 익명 진행중 2025-02-24 14:04:17 +09:00
09e79cb690 휴가 선택 활성화 수정정 2025-02-24 13:58:48 +09:00
0b0c948725 main.js 스크립트 오류 수정 2025-02-24 13:33:54 +09:00
Dang
1e185e81c6 Merge remote-tracking branch 'origin/main' into wordDict 2025-02-24 13:27:25 +09:00
02f0cda3b4 Merge branch 'khj' 2025-02-24 13:21:20 +09:00
213329198f 용어집 수정정 2025-02-24 13:20:48 +09:00
334aa3e22a menu, main 로딩 콘솔 주석처리 2025-02-24 12:47:31 +09:00
f2b364f4f8 휴가관리 수정 2025-02-24 12:08:02 +09:00
yoon
a766332bbc Merge branch 'main' into project-list 2025-02-24 10:25:18 +09:00
fe00b53e4f Merge branch 'main' of http://192.168.0.251:3000/localnet/localhost-front 2025-02-24 09:46:57 +09:00
d3832018d3 스크립트 에러 /콘솔 제거거 2025-02-24 09:45:11 +09:00
Dang
f92070018c Merge remote-tracking branch 'origin/main' into wordDict 2025-02-24 09:44:35 +09:00
yoon
40eb97e318 달력 css 수정 2025-02-24 09:40:37 +09:00
yoon
e27425d773 게시물 없을 때 멘트 변경 2025-02-22 17:54:54 +09:00
yoon
4ff4e43235 토스트 및 에러 찍히는거 삭제 2025-02-22 17:44:26 +09:00
yoon
fba884ef92 중복 오류 주석처리 2025-02-22 17:43:20 +09:00
yoon
c30bac2f2e 필요없는 거 삭제 2025-02-22 17:42:48 +09:00
yoon
f621efca29 Merge branch 'main' of http://192.168.0.251:3000/localnet/localhost-front 2025-02-22 14:05:20 +09:00
yoon
ddea199e85 custom css 수정 2025-02-22 14:03:39 +09:00
yoon
ac5cf6077c default img 추가 2025-02-22 14:02:05 +09:00
yoon
fc5f4463b1 select input 안나오는거 수정 2025-02-22 14:01:48 +09:00
ce1ecb5897 휴가 수정정 2025-02-21 16:21:26 +09:00
9bc34a02fc 에러메세지 2025-02-21 16:03:21 +09:00
yoon
c4b1a946ab profilePath 추가 2025-02-21 15:43:49 +09:00
yoon
0df7c5aec9 Merge branch 'main' into commuters 2025-02-21 14:53:24 +09:00
Dang
2fb0ca06dc 용어집 기본프로필 수정 2025-02-21 14:45:50 +09:00
yoon
2aa2c97ae5 출퇴근 현황 추가 2025-02-21 14:43:37 +09:00
yoon
8b45d5f048 defaultProfile 추가 2025-02-21 14:42:44 +09:00
6e4382f473 board-comment 다시머지지 2025-02-21 14:34:35 +09:00
692d363756 Merge remote-tracking branch 'origin/wordDict' 2025-02-21 14:32:42 +09:00
8d6ae54a9b 익명 수정 2025-02-21 14:18:08 +09:00
Dang
c612a8a90f 용어집 수정 및 삭제 2025-02-21 14:01:28 +09:00
ea1a3abf68 Merge branch 'board-comment' 2025-02-21 13:50:46 +09:00
6e4f1a1e81 디폴트 타입변경경 2025-02-21 13:29:24 +09:00
4076507fb7 Merge remote-tracking branch 'origin/main' into board-comment 2025-02-21 13:08:51 +09:00
b727750178 Merge branch 'khj' 2025-02-21 13:06:05 +09:00
c6bb8788d8 익명 null 수정 2025-02-21 13:04:53 +09:00
7bb2f48dda forminput min 속성추가가 2025-02-21 13:02:56 +09:00
cd01f0767e . 2025-02-21 12:58:11 +09:00
c88b01f78f 댓글 수정 삭제 보완 2025-02-21 12:29:16 +09:00
8d2799e0a1 댓글 수정 삭제 완 2025-02-21 11:54:54 +09:00
909be29d74 공휴일js 공통으로뻄 2025-02-21 10:46:46 +09:00
3779e63142 리스트 빨간글씨삭제 2025-02-21 10:36:12 +09:00
54b3db91ab . 2025-02-21 09:58:10 +09:00
a2551e0e58 . 2025-02-21 09:56:39 +09:00
c46bda283f Merge remote-tracking branch 'origin/main' into board-comment 2025-02-21 09:55:47 +09:00
3f4743cac2 댓글 비밀번호 진행중 2025-02-21 09:52:10 +09:00
04b7db1560 board-list-d머지 2025-02-21 09:50:09 +09:00
62361b7ae9 댓글 비밀번호 진행중 2025-02-21 03:50:17 +09:00
2da40134c3 . 2025-02-20 19:49:53 +09:00
638f5c4de5 . 2025-02-20 19:37:01 +09:00
87acf373d9 댓글 삭제 진행중 2025-02-20 18:58:53 +09:00
917b701663 . 2025-02-20 16:59:42 +09:00
a21bf8f6a1 대댓글 2025-02-20 16:57:27 +09:00
fdb2d81e53 대댓글 수정중 2025-02-20 16:54:37 +09:00
yoon
52809ba743 주석추가 2025-02-20 15:43:37 +09:00
yoon
a816978ba3 Merge branch 'main' into project-list 2025-02-20 15:25:54 +09:00
yoon
c1c4f68901 Merge branch 'main' of http://192.168.0.251:3000/localnet/localhost-front 2025-02-20 15:25:12 +09:00
yoon
b0239bacce Merge branch 'main' into project-list 2025-02-20 15:21:56 +09:00
Dang
018d5fa4f9 Merge remote-tracking branch 'origin/main' into wordDict 2025-02-20 15:21:46 +09:00
91d2492c13 휴가 삭제기능 2025-02-20 15:21:08 +09:00
4440946c74 익명일때 익명 체크박스 2025-02-20 15:20:07 +09:00
yoon
4175ecd290 검색 , 카테고리 별 나오게 2025-02-20 15:16:50 +09:00
f7617e11a6 댓글 좋아요 2025-02-20 15:12:12 +09:00
Dang
8e20066b87 용어집 수정 2025-02-20 14:19:35 +09:00
2226782662 Merge remote-tracking branch 'origin/main' into board-comment 2025-02-20 12:35:46 +09:00
e2d765cb15 게시판 수정, 삭제 완료 2025-02-20 12:33:10 +09:00
5c88ea3115 메인 비밀번호 입력창 2025-02-20 12:09:42 +09:00
5ece56e31d . 2025-02-20 10:56:29 +09:00
yoon
0aab690dc7 Merge branch 'main' into project-list 2025-02-20 09:21:58 +09:00
yoon
d6883c3a34 Merge branch 'main' into wordDict 2025-02-20 09:20:57 +09:00
yoon
7c784464a1 수정 로직 추가 2025-02-20 09:19:52 +09:00
yoon
521bb8ded4 click 이벤트 .stop 추가 2025-02-20 09:19:04 +09:00
bba4c60cb2 대댓글 수정중 2025-02-19 23:38:40 +09:00
yoon
1daafb3c75 주석 품 2025-02-18 15:46:11 +09:00
yoon
1b3339fa94 Merge branch 'main' into project-list 2025-02-18 15:05:30 +09:00
yoon
6f43657941 프로젝트 목록 미완성 2025-02-18 15:04:25 +09:00
8c29873731 머지 2025-02-18 14:46:20 +09:00
6d705d1093 boardCommentArea 2025-02-18 14:42:14 +09:00
656b7f92ca 페이지네이션 완료 2025-02-18 14:31:20 +09:00
2c28645488 머지 2025-02-18 14:30:14 +09:00
Dang
8b56ce4e42 작업 내용 설명 2025-02-18 14:26:24 +09:00
6890269faa 페이지네이션 진행중 2025-02-18 14:13:25 +09:00
eab2971c87 Merge remote-tracking branch 'origin/main' into board-comment 2025-02-18 13:49:51 +09:00
922b7e0904 ㅇㅇ 2025-02-18 13:19:21 +09:00
9a5261bbc7 Merge remote-tracking branch 'origin/main' into board-comment 2025-02-18 13:04:53 +09:00
27f5db0c6c Merge branch 'main' into wordDict 2025-02-18 11:19:36 +09:00
Dang
e4c8c19d7b Merge remote-tracking branch 'origin/main' into wordDict 2025-02-18 11:15:57 +09:00
46db001c56 Merge branch 'khj' 2025-02-18 11:12:53 +09:00
1304d38fdb Merge branch 'main' of http://192.168.0.251:3000/localnet/localhost-front 2025-02-18 11:12:35 +09:00
99b7296f00 투표 수정정 2025-02-18 11:12:06 +09:00
ce2ccde4fd 머지 2025-02-18 11:02:02 +09:00
ac97d5fcfb Merge branch 'main' of http://192.168.0.251:3000/localnet/localhost-front 2025-02-18 10:37:35 +09:00
2b57883edd Merge branch 'main' of http://192.168.0.251:3000/localnet/localhost-front 2025-02-18 10:33:47 +09:00
e376e5ff59 보드코맨트머지지 2025-02-18 10:33:45 +09:00
Dang
10d1aa4d8c 용어집 수정 2025-02-18 10:33:12 +09:00
yoon
deddbf7856 Merge branch 'main' into project-list 2025-02-18 10:32:49 +09:00
yoon
ec5f5e5e12 프로젝트 목록 2025-02-18 10:22:22 +09:00
ae4f4a807b Merge remote-tracking branch 'origin/main' into board-comment 2025-02-18 10:22:14 +09:00
25ae933ebe . 2025-02-18 10:21:22 +09:00
c65fc7f3a5 비밀번호 입력창 - 부모 페이지로 옮김 2025-02-18 00:48:22 +09:00
a90bdab5a0 boardProfile 수정전 2025-02-17 15:47:28 +09:00
Dang
0507663a1a Merge branch 'khj' into wordDict 2025-02-17 15:10:01 +09:00
yoon
e81127ee8a log, 프로젝트 수정 2025-02-17 15:07:10 +09:00
yoon
26041d4b90 프로젝트 수정을 위한 modelValue 추가 2025-02-17 15:06:43 +09:00
yoon
eead7902c4 import 경로 수정 2025-02-17 15:06:01 +09:00
Dang
62c97e0a18 용어집 수정 2025-02-17 15:04:56 +09:00
ee0b34dad0 qeditor수정정 2025-02-17 14:56:01 +09:00
5fe85dcd3b 댓글 좋아요 완료 2025-02-17 13:56:02 +09:00
499162639a 댓글 부분 id에서 commentId로 수정 2025-02-17 10:43:18 +09:00
yoon
d6789656ce userlist 로직 변경 2025-02-17 10:35:15 +09:00
2aaafcc782 Merge remote-tracking branch 'origin/main' into board-comment 2025-02-17 10:26:10 +09:00
28cfb7fcc5 . 2025-02-17 09:53:08 +09:00
2b7dabf27e . 2025-02-17 09:45:27 +09:00
yoon
3047011de8 프로젝트 목록 2025-02-17 09:29:23 +09:00
ff2524bcfa 세션 쿠키 유지를 위한 https 적용 2025-02-17 09:23:44 +09:00
5a4b770fa1 사원리스트 모달추가 2025-02-14 16:22:42 +09:00
fbc578c307 휴가수정,버튼추가 2025-02-14 14:50:23 +09:00
yoon
bbc8d8b6b9 useUserStore -> useUserInfoStore 2025-02-14 13:25:47 +09:00
yoon
a169d71c1d useUserStore -> useUserInfoStore 2025-02-14 13:24:26 +09:00
a1efaf1d90 . 2025-02-14 12:55:18 +09:00
a8d33a2555 . 2025-02-14 12:50:37 +09:00
cf4e56472a 프로필 부분 정리 2025-02-14 12:47:39 +09:00
8e438ff900 t용어집수정정 2025-02-14 12:16:24 +09:00
e14af4de3d Merge branch 'wordDict' 2025-02-14 11:07:39 +09:00
Dang
d71db81856 용어집 카테고리 등록 2025-02-14 11:02:45 +09:00
af36b11b6e . 2025-02-14 03:02:38 +09:00
737481cbae 대댓글완료 2025-02-14 03:01:30 +09:00
f7ca508a9c 휴가가리스트 모달 2025-02-13 16:25:20 +09:00
yoon
1e1834213b Merge branch 'main' into project-list 2025-02-13 14:58:51 +09:00
yoon
2304c3d4d3 grayscaleImg 2025-02-13 14:51:25 +09:00
yoon
264bb09dd5 Merge branch 'main' of http://192.168.0.251:3000/localnet/localhost-front 2025-02-13 14:50:54 +09:00
yoon
6edb911d43 프로젝트 목록 2025-02-13 14:48:51 +09:00
yoon
380fd9cb1d 모달 수정 2025-02-13 14:48:44 +09:00
yoon
9007e60f26 공통 불러오는 코드 수정 2025-02-13 14:48:25 +09:00
yoon
40dd760700 코드 수정 2025-02-13 14:48:12 +09:00
yoon
1e305ea86d class 수정 2025-02-13 14:48:05 +09:00
yoon
3378dcf93c is-row 추가 2025-02-13 14:47:46 +09:00
yoon
18d4095f1b class 수정 2025-02-13 14:47:24 +09:00
yoon
66ef5d6302 scrollbar-none 추가 2025-02-13 14:47:13 +09:00
yoon
c8ca8f936f 주석 변경 2025-02-13 14:46:38 +09:00
yoon
558008acc5 commonApi 로직 수정 2025-02-13 14:38:07 +09:00
04ab0325b7 Merge remote-tracking branch 'origin/main' into board-comment 2025-02-13 13:27:51 +09:00
7426dd8926 좋아요/싫어요버튼 머지 2025-02-13 13:25:22 +09:00
e4c8b0aa1f 휴가관리 사원프로필 2025-02-13 13:20:44 +09:00
7774bfe80a 대댓글 진행중 2025-02-13 13:20:11 +09:00
e81229db4c 댓글 조회 및 등록 2025-02-13 12:03:43 +09:00
cfa46c0a6d 개발용도의 쿠키 세팅 2025-02-13 11:54:38 +09:00
34a1c8360d . 2025-02-12 23:48:33 +09:00
12538f0046 댓글등록 진행중 2025-02-12 23:43:38 +09:00
f33cada52f 익명일때 코멘트 익명 인풋 완료 2025-02-12 17:39:35 +09:00
e4455e94e5 휴가 사원리스트추가 2025-02-11 16:18:05 +09:00
024807ccda board-view-content 머지 2025-02-11 15:10:02 +09:00
4daab961cf 휴가저장장 2025-02-11 15:03:25 +09:00
yoon
8ab9e79b5a userlist 코드 수정 2025-02-11 12:59:49 +09:00
yoon
40c2a166a4 userList filter css 추가 2025-02-11 12:59:40 +09:00
yoon
724f0afc61 headers 경로 추가 -> 응답 다르게 하기 2025-02-11 10:33:40 +09:00
yoon
c3b2bcec66 응답 인터셉터 수정 2025-02-11 10:33:09 +09:00
yoon
7a9daa4732 menu 주석 풀기 2025-02-11 10:32:53 +09:00
1fed22aba5 css수정정 2025-02-11 10:24:32 +09:00
baed45ebc9 css정리 2025-02-11 10:21:59 +09:00
5a64d77502 머지 2025-02-10 16:24:11 +09:00
yoon
3cb3281a86 카테고리 목록 불러오기 수정 2025-02-10 16:10:28 +09:00
yoon
8fcc702d9b Merge branch 'main' into project-list 2025-02-10 16:08:45 +09:00
yoon
d96de3745e Merge branch 'main' into wordDict 2025-02-10 16:07:51 +09:00
yoon
34acf8aedb 프로젝트 목록 2025-02-10 16:07:24 +09:00
yoon
608f7be852 label, value 형식으로 수정 2025-02-10 16:07:09 +09:00
yoon
d47e3f0d6d yearCategory, cateList 추가 2025-02-10 16:06:22 +09:00
Dang
8fd19938ba 용어집 등록 2025-02-10 16:04:33 +09:00
296dd2452a 보드프로필 2025-02-10 15:57:23 +09:00
yoon
62047cbf80 Merge branch 'main' into project-list 2025-02-10 15:13:16 +09:00
yoon
4ebab7cf95 disabled 추가 2025-02-10 15:11:07 +09:00
yoon
23cd85d727 프로젝트 목록 2025-02-10 14:59:56 +09:00
yoon
2e270291f5 Merge branch 'main' into project-list 2025-02-10 14:47:04 +09:00
yoon
9d1911be9a Merge branch 'main' into login 2025-02-10 14:45:39 +09:00
yoon
8a15c9c1cc 프로젝트 목록 추가 2025-02-10 14:45:13 +09:00
yoon
4413001d9d 로그인 로직 변경 2025-02-10 14:44:29 +09:00
yoon
6704f5531a 응답 인터셉터 추가 2025-02-10 14:41:37 +09:00
c8dbd53e3a 보드프로필 머지 2025-02-10 14:02:27 +09:00
fee7b52384 board-view-content 머지 2025-02-10 14:00:30 +09:00
yoon
8427dce4cc Merge branch 'main' into login 2025-02-10 10:28:24 +09:00
yoon
de1a6e19c5 비밀번호 재설정 2025-02-10 10:27:53 +09:00
f707329f79 console제거거 2025-02-07 14:59:58 +09:00
cb67307a62 user수정정 2025-02-07 14:58:14 +09:00
yoon
58fdb1f673 기존 비밀번호 체크 2025-02-07 14:53:17 +09:00
7d03da5097 Merge branch 'khj' 2025-02-07 14:48:04 +09:00
a0683ec69f Merge branch 'main' of http://192.168.0.251:3000/localnet/localhost-front 2025-02-07 14:45:50 +09:00
f40e3ed7d1 수정 2025-02-07 14:45:17 +09:00
05dca89be0 휴가저장,리스트 2025-02-07 14:40:25 +09:00
yoon
792fde6eac package-lock 2025-02-07 13:54:01 +09:00
yoon
3f7cf45f4c log 삭제 2025-02-07 13:46:05 +09:00
yoon
74b29bede3 Merge branch 'main' into login 2025-02-07 13:37:25 +09:00
854c0c91e9 userList수정 2025-02-07 10:30:18 +09:00
c2ff50061a Merge branch 'main' of http://192.168.0.251:3000/localnet/localhost-front 2025-02-07 09:53:18 +09:00
yoon
646c775d6a Merge branch 'main' into wordDict 2025-02-07 09:39:57 +09:00
yoon
b3bfe27736 간격 설정 2025-02-07 09:37:19 +09:00
Dang
e61bcd20ce 용어집 등록 2025-02-06 16:16:25 +09:00
a317b12732 Merge branch 'main' of http://192.168.0.251:3000/localnet/localhost-front 2025-02-06 14:36:31 +09:00
yoon
32d68f198b user 정보 2025-02-06 14:20:51 +09:00
yoon
81d68ea2f1 로그인 한 user 프로필 가져옴 2025-02-06 14:20:30 +09:00
yoon
30a111c8e0 로그인 시 user정보 2025-02-06 14:20:13 +09:00
69afdab946 용어집 카테고리 수정 2025-02-06 13:26:07 +09:00
yoon
5d1f220f14 비밀번호 재설정 2025-02-06 10:49:08 +09:00
67d5bede32 용여집 수정정 2025-02-06 10:29:08 +09:00
yoon
f3ca41f9ad Merge branch 'main' into login 2025-02-06 09:26:50 +09:00
yoon
c95a2ae80b Merge branch 'main' into wordDict 2025-02-06 09:25:59 +09:00
yoon
4329d9febd Merge branch 'main' into board-view-content 2025-02-06 09:25:12 +09:00
yoon
7304b9f03b 비밀번호 재설정 추가 2025-02-06 09:24:13 +09:00
yoon
0a91dbef09 비밀번호 힌트 공통에서 받아오는 걸로 변경 2025-02-06 09:24:01 +09:00
yoon
ffd23da7f4 title 두껍게 2025-02-06 09:23:31 +09:00
yoon
9d96f2141d 오늘 날짜 이후로 선택 X 2025-02-06 09:23:11 +09:00
yoon
b841029bbe 공통 api 2025-02-06 09:22:50 +09:00
67bb83277a 게시판 리스트 번호 수정 2025-02-04 16:21:07 +09:00
68c096b43f 게시판리스트 수정정 2025-02-04 16:15:47 +09:00
815019e02a Merge remote-tracking branch 'origin/main' into board-view-content 2025-02-04 16:06:43 +09:00
552f307c45 좋아요 작업중 2025-02-04 16:05:48 +09:00
Dang
69b5216963 용어집 입력 및 입력 수정 중 2025-02-04 15:59:28 +09:00
0209dcbdc0 Merge branch 'main' of http://192.168.0.251:3000/localnet/localhost-front 2025-02-04 15:56:10 +09:00
1de0c882dd 보드리스트 수정 및 보드카드 원복 2025-02-04 15:56:08 +09:00
yoon
9ee69e6730 api 2025-02-04 14:22:49 +09:00
57c81b36a5 날짜 수정 완료 2025-02-04 13:38:26 +09:00
yoon
e038c84baf Merge branch 'main' into login 2025-02-04 13:06:30 +09:00
Dang
6f78a1e921 Merge remote-tracking branch 'origin/main' into wordDict 2025-02-04 11:22:42 +09:00
233a345ae2 용어집 수정ㅇㅇㅇ 2025-02-04 11:12:09 +09:00
yoon
97cebc26d8 logout, 아이콘 변경 2025-02-04 11:11:24 +09:00
yoon
21f48d0e27 로그인 했을 시 로그인,회원가입 페이지 이동X 2025-02-04 11:10:31 +09:00
Dang
ff74a7477b Merge remote-tracking branch 'origin/main' into wordDict 2025-02-04 11:03:11 +09:00
d3170637c0 Merge branch 'main' into khj 2025-02-04 11:01:47 +09:00
822223ff0d Merge branch 'main' of http://192.168.0.251:3000/localnet/localhost-front 2025-02-04 11:01:10 +09:00
64b0de3e7f 용어집 수정 2025-02-04 10:59:49 +09:00
d1aa5fb438 날짜 변환전, 좋아요 해결완료 2025-02-04 10:57:18 +09:00
8cfffa6610 글쓰기 카테고리 공통코드 수정정 2025-02-03 16:18:31 +09:00
927b742fab Merge branch 'wordDict' 2025-02-03 16:06:46 +09:00
Dang
e6efc307ca 용어집 목록 수정 2025-02-03 15:57:03 +09:00
yoon
66061435e1 Merge branch 'main' into login 2025-02-03 15:12:24 +09:00
yoon
4ffb98b7cf 줄 맞춤 2025-02-03 15:11:25 +09:00
yoon
4258c57512 remember 체크 2025-02-03 15:07:50 +09:00
yoon
a4cb9936ab watchEffect -> watch 2025-02-03 15:07:29 +09:00
8e42b6b2b5 카드 넓이고정 2025-02-03 14:57:34 +09:00
9937e8591e 게시판머지2 2025-02-03 14:51:51 +09:00
f853c4e25d board-view-profile1 머지 2025-02-03 14:31:48 +09:00
dbc0ad83ae 보드리스트 width 수정 2025-02-03 10:48:21 +09:00
161 changed files with 19224 additions and 8621 deletions

7
.env.dev Normal file
View File

@ -0,0 +1,7 @@
VITE_DOMAIN = https://192.168.0.251:5100/
# VITE_LOGIN_URL = http://localhost:10325/ms/
VITE_SERVER = https://192.168.0.251:10300/
VITE_API_URL = https://192.168.0.251:10300/api/
VITE_TEST_URL = https://192.168.0.251:10300/test/
VITE_SERVER_IMG_URL = https://192.168.0.251:10300/upload/img/
VITE_KAKAO_MAP_KEY=6f092e8f45ee81186bb6d8408f66a492

7
.env.mine Normal file
View File

@ -0,0 +1,7 @@
VITE_DOMAIN = http://localhost:5173/
# VITE_LOGIN_URL = http://localhost:10325/ms/
VITE_SERVER = http://localhost:10325/
VITE_API_URL = http://localhost:10325/api/
VITE_TEST_URL = http://localhost:10325/test/
VITE_SERVER_IMG_URL = http://localhost:10325/upload/img/
VITE_KAKAO_MAP_KEY=6f092e8f45ee81186bb6d8408f66a492

View File

@ -32,7 +32,6 @@
<link rel="stylesheet" href="/css/font.css" /> <link rel="stylesheet" href="/css/font.css" />
<!-- Icons --> <!-- Icons -->
<link rel="stylesheet" href="/vendor/fonts/boxicons.css" /> <link rel="stylesheet" href="/vendor/fonts/boxicons.css" />
<link rel="stylesheet" href="/vendor/fonts/fontawesome.css" /> <link rel="stylesheet" href="/vendor/fonts/fontawesome.css" />
@ -74,7 +73,7 @@
<script src="/vendor/libs/hammer/hammer.js"></script> <script src="/vendor/libs/hammer/hammer.js"></script>
<script src="/vendor/libs/i18n/i18n.js"></script> <script src="/vendor/libs/i18n/i18n.js"></script>
<script src="/vendor/libs/typeahead-js/typeahead.js"></script> <script src="/vendor/libs/typeahead-js/typeahead.js"></script>
<!-- <script src="/vendor/js/menu.js"></script> --> <script src="/vendor/js/menu.js"></script>
<!-- endbuild --> <!-- endbuild -->
<!-- Vendors JS --> <!-- Vendors JS -->

41
jenkinsfile Normal file
View File

@ -0,0 +1,41 @@
pipeline {
agent any
tools {
nodejs 'nodejs22'
}
stages {
stage('Build'){
steps {
bat 'npm install'
bat 'npm ci'
bat 'npm run build'
}
}
stage('Deploy') {
steps {
// 로컬 Nginx 서버에 빌드 파일 배포
bat '''
:: 루트경로 설정
set NGINX_ROOT=C:\\nginx\\html
:: 기존 빌드 삭제
if exist "%NGINX_ROOT%\\dist" rmdir /s /q "%NGINX_ROOT%\\dist"
:: 빌드 파일 복사
xcopy /s /y dist\\* "%NGINX_ROOT%\\dist\\"
'''
}
}
stage('Restart Server!') {
steps {
// 로컬 Nginx 서버 재실행
bat '''
net stop localnginx
ping -n 5 127.0.0.1 > nul
net start localnginx
ping -n 5 127.0.0.1 > nul
'''
}
}
}
}

28
localhost-key.pem Normal file
View File

@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDnTJneuEjlCk/g
mzUQyeI9xWdr5TiYz+/HVdIR91PlswECLXiW+j420ARQ+Dpx8JY1sts9kMki/jnD
8cNFMwSOnJADwyW+bZZYAdGd/OpyyNQWjm903pKntBKDYULebEnviMDQz7+M6J9Y
cVqWSRTMR6VizSYIQ0vIUqQZaISp/TKLvTssjLjwz6DIYtZ2GQ06lAZYmvX6UhSl
32xJQ6GWf4Jnemn/2bKYFGk88d4ORfrhpD1JV3lGQGk0HU/pI8R017pKxq9lq/c4
d8hqN0CdkQUv0lFw6DQgm3etOVOajlY4eDj+Q3mDCERT8meH1PXfuTsHBtTMHxoA
iRtu4YoZAgMBAAECggEBAMti1DrAGXktpCeA0xy8KTDgEJ0TprzYu6Owl1QtA50L
1msvyMYZrfNM3z7Dx8DBKZR2fcqZMgSPQARI5shGoE825HwqcVoNyxIAJ26hIxdj
+PsMrH076gGFmnHkaIRk/G6g9cunomwpcVS3+EwGXz9yEL/cXQEPC+hOovWkrmM6
Ec1oAsqs9DjrK+HzOOcaBuv0Rz9pI7Gob5LQAp8tqOCI4CvP6sdfooSsucCoqB6V
xQAakXbsidamWcLtYkTjY2zYVhHvVMk1H5krfgrsoGIaym/QTnk+YQYFd5jt4FiJ
ziLZXiZJOeJXJVdlAcJF9aUlO48OAKJeBoq3NkcbxbECgYEA+a2pJ6BN8xKvWyZJ
QnpWL5irVKUJF7l1cFvaNJJu4XMS3CfJqdA9X0b7Zuu/8zOdj+5eNpK6Mpz65+mx
+/ToYNYoMewFXlfDpcIpT4FdBJAKsKMua2UlTzOI7DxSrcAGD0nItK3ZovpzmNJo
H90maU0gib0CSsFVvsAsnVVSawcCgYEA7SfP8tc23txMbLzz+9DjlmeqT2v1XI/P
QZEwCO6AIjbbJw3iFYjIgkd86gVGRwIdYEfNoiIk8KT4dMsW4jiwalFa/fA+HW69
pqMf1PFnxNPZim592EANVjzzkN4jm63QzIAFiGcN2K99ltjUWrfrzLkkiFR6ENHF
dgIpWTyAed8CgYBH/CCr8xTmQvnZzsUKbJkSqfKjud1QQMEyPtk/lQRw7at/W91R
n19CbAWpm8jAxp3j1HbHRzB1zTqtyHvvR6ID4Vq/Yev+UlFvJfahHIwD97+NQ87r
WcMS/am5an7v51AX8k7ygLkhuxG++tLYdPtRGtKJw7u4b9tX3rI+Pk4/2wKBgQCn
CIZ3ZMuZ1hHh+Ifj0bGqSqNywvgS1JtGdAsgD1OiRX6/mBCn2CpZUB6T+VkRRFUK
bihQTLo14Au6vxwEA6eFin2LI72sH0ZmarhN1CWhRREQZlguipaaKd3nJ/5udNL+
ZiD/fI4NEzVinJ+csbPcAn7PoqhC1my8fDNBTdKzgwKBgQCvH0MEpkZefqN82CNn
CuJeQYb48mkFgihICeTsfIeG7XsGqfCOlzbJqxCbTX+Na7FUdtmtJUznK+rVGOPh
p+pAw8RbZSIvgzCO1vv0wSHsXxXsieOgwJPZeQqsBWhRs77Ggf9jhIzxcQJuIor3
l7Nxg0eoiqP/rYFyOh83nebPQg==
-----END PRIVATE KEY-----

BIN
localhost.p12 Normal file

Binary file not shown.

26
localhost.pem Normal file
View File

@ -0,0 +1,26 @@
-----BEGIN CERTIFICATE-----
MIIEUjCCArqgAwIBAgIQA9mbF03CznoBZ2TyJTPO8jANBgkqhkiG9w0BAQsFADCB
jTEeMBwGA1UEChMVbWtjZXJ0IGRldmVsb3BtZW50IENBMTEwLwYDVQQLDChERVNL
VE9QLVNHRkwzNExcaG9zdHBhcmtAREVTS1RPUC1TR0ZMMzRMMTgwNgYDVQQDDC9t
a2NlcnQgREVTS1RPUC1TR0ZMMzRMXGhvc3RwYXJrQERFU0tUT1AtU0dGTDM0TDAe
Fw0yNTAyMTQwMzU2MTJaFw0yNzA1MTQwMzU2MTJaMFwxJzAlBgNVBAoTHm1rY2Vy
dCBkZXZlbG9wbWVudCBjZXJ0aWZpY2F0ZTExMC8GA1UECwwoREVTS1RPUC1TR0ZM
MzRMXGhvc3RwYXJrQERFU0tUT1AtU0dGTDM0TDCCASIwDQYJKoZIhvcNAQEBBQAD
ggEPADCCAQoCggEBAOdMmd64SOUKT+CbNRDJ4j3FZ2vlOJjP78dV0hH3U+WzAQIt
eJb6PjbQBFD4OnHwljWy2z2QySL+OcPxw0UzBI6ckAPDJb5tllgB0Z386nLI1BaO
b3Tekqe0EoNhQt5sSe+IwNDPv4zon1hxWpZJFMxHpWLNJghDS8hSpBlohKn9Mou9
OyyMuPDPoMhi1nYZDTqUBlia9fpSFKXfbElDoZZ/gmd6af/ZspgUaTzx3g5F+uGk
PUlXeUZAaTQdT+kjxHTXukrGr2Wr9zh3yGo3QJ2RBS/SUXDoNCCbd605U5qOVjh4
OP5DeYMIRFPyZ4fU9d+5OwcG1MwfGgCJG27hihkCAwEAAaNeMFwwDgYDVR0PAQH/
BAQDAgWgMBMGA1UdJQQMMAoGCCsGAQUFBwMBMB8GA1UdIwQYMBaAFN7IkO4WB6E9
uTxB+KENPr8pN9V4MBQGA1UdEQQNMAuCCWxvY2FsaG9zdDANBgkqhkiG9w0BAQsF
AAOCAYEAa9kWqz5NvJo1+9lzTM+QwjeRL7wsfeygdwIx3PRn/0bbyglUO+MhR6oK
cbjzpKj+2C5sWuuNSIOGcU95Dnh6ekQtxjSY3j7gbwOiYmwYj4LVRF9KIeGQgW72
kHA+tnuEsAhe33mloJhGjrZ/cqkxPz31foVpOpeP0l85NTzXGfyDjePivlgfbCUT
8juBEIGD1Go3PTrLoNC0P/1lJAgc1+lGEY2veGQNMqy6TXIhLLHMuXdSEDqQJxjB
N6fNzfZh163jgI4UCpmowoLp6oO5iOlM3cxzsfwGpubf7W9nUOyAO5B4VzsTvqYe
MLfiUKZXlwUb9eyhIhk0UhgCM4IelcRMUH5nLDn6a2Pyu3bs4TpJ1zTmRZt7PjsX
0HllN2/xkp2XRdSLutGTrya5zqo4nLaDa67sTt5WhDp+JRgA3rb5Sgcw78pYEfFq
5IGuKZsuSMy6qZFbTAJVINPKwkH6eBAQcr3PyyCMKdJDFkeVYeuqef5N2u/GpGKO
DQ0E7Vhc
-----END CERTIFICATE-----

3105
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -5,7 +5,8 @@
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite --host 0.0.0.0 --mode dev", "dev": "vite --host 0.0.0.0 --mode dev",
"build": "vite build --mode prod", "mine": "vite --host 0.0.0.0 --mode mine",
"build": "vite build --mode dev",
"preview": "vite preview", "preview": "vite preview",
"lint": "eslint . --fix", "lint": "eslint . --fix",
"format": "prettier --write src/" "format": "prettier --write src/"
@ -18,8 +19,10 @@
"@popperjs/core": "^2.11.8", "@popperjs/core": "^2.11.8",
"@tinymce/tinymce-vue": "^5.1.1", "@tinymce/tinymce-vue": "^5.1.1",
"@vueup/vue-quill": "^1.2.0", "@vueup/vue-quill": "^1.2.0",
"@vueuse/core": "^13.0.0",
"axios": "^1.7.9", "axios": "^1.7.9",
"bootstrap": "^5.3.3", "bootstrap": "^5.3.3",
"bootstrap-icons": "^1.11.3",
"dayjs": "^1.11.13", "dayjs": "^1.11.13",
"dompurify": "^3.2.3", "dompurify": "^3.2.3",
"flatpickr": "^4.6.13", "flatpickr": "^4.6.13",
@ -29,9 +32,11 @@
"pinia-plugin-persist": "^1.0.0", "pinia-plugin-persist": "^1.0.0",
"quill": "^2.0.3", "quill": "^2.0.3",
"upload-images-converter": "^2.0.2", "upload-images-converter": "^2.0.2",
"vite-plugin-mkcert": "^1.17.6",
"vue": "^3.5.13", "vue": "^3.5.13",
"vue-flatpickr-component": "^11.0.5", "vue-flatpickr-component": "^11.0.5",
"vue-router": "^4.4.5" "vue-router": "^4.4.5",
"vue3-kakao-maps": "^2.3.10"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.14.0", "@eslint/js": "^9.14.0",

View File

@ -1,6 +1,2 @@
/* 여기에 dark css 작성 */ /* 여기에 dark css 작성 */
.display-block {
display: block !important;
}

View File

@ -1,6 +1,834 @@
/* 여기에 light css 작성 */ /* 여기에 light css 작성 */
.opacity-50 {
.display-block { opacity: 0.5;
display: block !important; }
/* board */
.board-content img {
max-width: 100% !important;
height: auto !important;
display: block;
object-fit: contain;
}
.board-content table {
max-width: 100%;
overflow-x: auto;
display: block;
}
.btn.big {
width: 70px;
height: 70px;
font-size: 18px;
}
@keyframes new {
0% {
background-color: #ffcc00;
}
50% {
background-color: red;
}
100% {
background-color: #ffcc00;
}
}
.box-new {
animation: new 1s infinite; /* 1초마다 반복 */
}
/* board end */
/* Qeditor */
.ql-editor {
min-height: 300px;
font-family: 'Nanum Gothic', sans-serif;
}
/* Qeditor end */
/* 에러페이지 */
/* 전체 화면을 덮는 스타일 */
.error-page {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background: #fff;
display: flex;
justify-content: center;
align-items: center;
text-align: center;
color: #000;
font-family: 'Poppins', sans-serif;
z-index: 9999 !important;
}
/* 오류 메시지 컨텐츠 */
.error-content {
text-align: center;
animation: fadeIn 0.8s ease-in-out;
}
/* 에러 이미지 */
.error-image {
width: 280px; /* 이미지 크기 */
margin-bottom: 20px;
}
/* 에러 코드 스타일 */
.error-content h1 {
font-size: 6rem;
font-weight: bold;
color: #ff8c00; /* 오렌지 */
text-shadow: 2px 2px 8px rgba(255, 140, 0, 0.3);
margin-bottom: 60px;
}
/* 홈으로 돌아가기 버튼 */
.home-btn {
display: inline-block;
padding: 10px 28px;
font-size: 1rem;
font-weight: bold;
text-decoration: none;
color: #fff;
background: rgba(105, 108, 255, 0.9);
border-radius: 30px;
transition: 0.3s ease-in-out;
box-shadow: 0 4px 15px rgba(105, 108, 255, 0.5);
}
/* 버튼 호버 효과 */
.home-btn:hover {
background: linear-gradient(90deg, orange, #ff8c00);
box-shadow: 0 0 20px rgba(255, 140, 0, 1);
transform: scale(1.05);
color: #fff;
}
/* 페이드 인 애니메이션 */
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* 휴가 */
.fc-daygrid-event {
pointer-events: none !important;
}
/* 이벤트 선 없게 */
.fc-event {
border: none;
}
/* 오전 반차 그래프 (왼쪽 절반) */
.fc-daygrid-event.half-day-am {
width: 50% !important;
height: 8px !important;
border-radius: 2px !important;
font-size: 0px !important;
margin-left: -0.5% !important;
}
/* 오후 반차 그래프 (오른쪽 절반) */
.fc-daygrid-event.half-day-pm {
width: 50% !important;
height: 8px !important;
margin-left: auto !important;
border-radius: 2px !important;
font-size: 0px !important;
margin-right: -0.5% !important;
}
/* 연차 그래프 (풀) */
.fc-daygrid-event.full-day {
width: 100% !important;
height: 8px !important;
margin-left: auto !important;
border-radius: 2px !important;
font-size: 0px !important;
}
/* 공휴일,일요일 색상 */
.fc-day-sun .fc-daygrid-day-number,
.fc-col-header-cell:first-child .fc-col-header-cell-cushion {
color: #ff4500 !important;
}
/* 토요일 색상 */
.fc-day-sat .fc-daygrid-day-number,
.fc-col-header-cell:last-child .fc-col-header-cell-cushion {
color: #6076e0 !important;
}
/* 캘린더 날짜 왼쪽 상단 위치하게 */
.fc-daygrid-day-number {
margin-right: auto;
}
/* 데이트피커 뾰족없게 */
.flatpickr-calendar:before,
.flatpickr-calendar:after {
display: none !important;
}
/* 기본 스타일은 그대로 두고, 데이트피커 인풋의 추가 스타일 정의 */
.fc-toolbar-title {
cursor: pointer;
}
.fc-today-button {
cursor: pointer !important;
}
/* 클릭 가능한 날짜 */
.fc-daygrid-day.clickable {
cursor: pointer;
transition: background-color 0.2s ease-in-out;
}
/* 마우스를 올렸을 때 효과 */
.fc-daygrid-day.clickable:hover {
background-color: rgba(0, 0, 0, 0.05); /* 연한 배경 효과 */
}
/* 주말 (토요일, 일요일) 및 공휴일 */
.fc-day-sat-sun {
cursor: not-allowed !important;
}
/* 과거 날짜 (오늘 -7일일) */
.fc-daygrid-day.past {
cursor: not-allowed !important;
}
/* 기본 이벤트 스타일 */
.fc-daygrid-event {
border: none !important;
border-radius: 4px;
}
/* 오전 반차 활성화 영역 (왼쪽 절반) */
.selected-event.half-day-am {
width: 50% !important;
left: 0 !important;
border-top-left-radius: 4px;
border-bottom-left-radius: 4px;
}
/* 오후 반차 활성화 영역 (오른쪽 절반) */
.selected-event.half-day-pm {
width: 50% !important;
margin-left: auto !important;
border-top-right-radius: 4px;
border-bottom-right-radius: 4px;
}
/* 휴가 모달 */
.vac-modal-dialog {
background: none !important;
box-shadow: none !important;
display: flex;
align-items: flex-end;
justify-content: center;
width: 100%;
height: 100%;
padding-bottom: 20px;
}
.vac-modal-content {
background: #fff;
padding: 20px;
box-shadow:
0px -4px 5px rgba(0, 0, 0, 0.1),
0px 4px 0px rgba(0, 0, 0, 0);
max-width: 500px;
width: 100%;
position: relative;
}
.vac-modal-body {
max-height: 180px;
overflow-y: auto;
}
.vac-modal-text {
font-size: 14px;
text-align: center;
margin-bottom: 20px;
}
.count-container {
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
margin-bottom: 10px;
}
.count-value {
font-size: 23px;
font-weight: bold;
min-width: 50px;
text-align: center;
}
.custom-button {
background: none;
border: none;
width: 55px;
height: 55px;
font-size: 26px;
color: white;
border-radius: 50%;
cursor: pointer;
transition: all 0.2s;
}
.vac-modal-title {
margin-bottom: 10px;
}
.vacation-item {
display: flex;
align-items: center;
font-size: 16px;
font-weight: bold;
margin-bottom: 8px;
padding: 5px 10px;
border-radius: 5px;
background: #f9f9f9;
}
.close-btn {
position: absolute;
top: 10px;
right: 10px;
background: none;
border: none;
font-size: 18px;
cursor: pointer;
}
.close-btn:hover {
color: #525252;
}
.count-btn {
font-size: 17px;
padding: 2px 10px;
border: none;
background: #2c3e50;
color: white;
border-radius: 5px;
cursor: pointer;
}
.count-btn:hover {
background: #1d2c44;
}
.count-btn:disabled {
background: #cccccc;
cursor: not-allowed;
}
.custom-button-container {
display: flex;
justify-content: flex-end;
align-items: center;
}
.custom-button {
background: none;
border: none;
padding: 10px;
cursor: pointer;
}
.custom-button i {
color: #282538;
font-size: 25px;
}
.custom-button:hover i {
color: #ff0800;
}
.custom-button:disabled {
cursor: not-allowed;
}
/* 휴가 사원프로필 */
.profile-list {
cursor: pointer;
}
/* 오전/오후반차,저장버튼 */
/* 버튼 기본 스타일 */
.vac-btn {
transition: all 0.2sease-in-out;
border: 2px solid transparent;
}
/* 마우스를 올렸을 때 */
.vac-btn:hover {
filter: brightness(90%);
box-shadow: 0px 4px 10px rgba(0, 0, 0, 0.2);
transform: scale(1.05);
}
/* 버튼이 눌렸을 때 */
.vac-btn:active {
transform: scale(0.9);
box-shadow: 0px 2px 5px rgba(0, 0, 0, 0.2);
}
/* 선택된 (눌린) 버튼 */
.vac-btn.active {
box-shadow: 0px 4px 15px rgba(224, 224, 224, 0.3);
transform: scale(1.1);
}
.vac-btn-warning {
color: #fff;
background-color: #ffc144;
border-color: #ffc144;
box-shadow: 0 0.125rem 0.25rem 0 rgba(255, 171, 0, 0.4);
font-size: 28px;
}
/* AM 버튼 (선택된 상태) */
.vac-btn-warning.active {
background-color: #ff7300 !important;
color: #fff;
}
.vac-btn-info {
color: #fff;
background-color: #03c3ec;
border-color: #03c3ec;
box-shadow: 0 0.125rem 0.25rem 0 rgba(3, 195, 236, 0.4);
font-size: 28px;
}
/* PM 버튼 (선택된 상태) */
.vac-btn-info.active {
background-color: #0b5ed7 !important;
color: white;
}
/* 풀 연차 버튼 스타일 */
.vac-btn-primary {
color: #fff;
background-color: #49d46a; /* 녹색 */
border-color: #49d46a;
box-shadow: 0 0.125rem 0.25rem 0 rgba(40, 167, 69, 0.4);
font-size: 28px;
transition: all 0.2s ease-in-out;
}
/* 풀 연차 버튼 활성화 스타일 */
.vac-btn-primary.active {
background-color: #009124 !important;
color: #fff;
border: 3px solid #91d091 !important;
box-shadow: 0px 4px 15px rgba(0, 0, 0, 0.3);
transform: scale(1.1);
}
/* 풀 연차 버튼이 눌렸을 때 효과 */
.vac-btn-primary:active {
transform: scale(0.9);
box-shadow: 0px 2px 5px rgba(0, 0, 0, 0.2);
}
/* 버튼 기본 */
.vac-btn-success {
transition: all 0.2s ease-in-out;
background-color: #871919 !important;
color: white;
border: 2px solid transparent;
font-size: 30px;
}
/* 버튼 활성화 */
.vac-btn-success.active {
background-color: #ff0000 !important;
color: white !important;
border: 3px solid #eb9f9f !important;
box-shadow: 0px 4px 15px rgba(0, 0, 0, 0.3);
transform: scale(1.1);
}
/* 버튼 비활성화 */
.vac-btn-success.disabled {
background-color: #bbb8b8 !important;
color: white !important;
cursor: not-allowed !important;
box-shadow: none;
transform: none;
opacity: 0.5;
}
/* 작은 화면에서 버튼 크기 조정 */
@media (max-width: 1700px) {
.count-btn {
width: 26px;
height: 26px;
font-size: 15px;
}
.count-container {
display: flex;
align-items: center;
justify-content: center;
gap: 0px;
margin-bottom: 8px;
}
.count-value {
font-size: 15px;
}
.custom-button {
width: 45px;
height: 45px;
font-size: 22px;
}
.vac-grant-modal-title {
font-size: 18px;
}
.vac-modal-text {
font-size: 12px;
}
.vac-modal-title {
font-size: 15px;
margin-bottom: 10px;
}
.vacation-item {
font-size: 13px;
text-align: center;
margin-bottom: 5px;
}
.vac-btn {
width: 40px;
height: 40px;
font-size: 18px;
}
.vac-btn-success {
font-size: 20px;
width: 50px;
height: 50px;
}
}
@media (max-width: 1500px) {
.vac-grant-modal-title {
font-size: 14px;
}
.vac-modal-text {
font-size: 11px;
}
.vac-modal-title {
font-size: 13px;
margin-bottom: 10px;
}
.close-btn {
top: 5px;
right: 5px;
font-size: 13px;
}
.vacation-item {
font-size: 11px;
text-align: center;
margin-bottom: 5px;
}
.vac-btn {
width: 10px;
height: 10px;
font-size: 12px;
}
.vac-btn-success {
font-size: 15px;
width: 40px;
height: 40px;
}
}
.grayscaleImg {
filter: grayscale(100%);
}
/* scrollbar 안보이게 */
.scrollbar-none {
scrollbar-width: none;
}
/* project list */
.hidden-start-input {
position: absolute;
top: 103%;
left: 20%;
width: 100%;
height: 40px;
opacity: 0;
}
.hidden-end-input {
position: absolute;
top: 113.3%;
left: 20%;
width: 100%;
height: 40px;
opacity: 0;
}
.map {
top: -160px;
left: 90px;
}
@keyframes sparkle {
0% {
color: #ffcc00;
}
50% {
color: red;
}
100% {
color: #ffcc00;
}
}
.bxs-map {
animation: sparkle 1s infinite; /* 1초마다 반복 */
}
.popover-close {
position: absolute;
top: 1rem;
right: 1rem;
z-index: 2;
background-color: #fff !important;
box-shadow: 0 0.125rem 0.25rem rgba(161, 172, 184, 0.4);
transition: all 0.23s ease 0.1s;
transform: translate(23px, -25px);
}
.popover-close:hover {
opacity: 1;
outline: none;
transform: translate(20px, -20px);
}
.end-project {
background-color: #ddd !important;
}
/* project list end */
/* commuters */
.commuter-list {
max-height: 450px;
overflow-y: auto;
scrollbar-width: none;
}
.fc-daygrid-day[data-has-commuters='true'] {
cursor: pointer;
}
/* commuters end */
/* Scroll Button */
.scroll-top-btn {
bottom: 40px;
right: 21.7%;
transition:
opacity 0.4s ease,
visibility 0.4s ease,
transform 0.4s ease;
}
.scroll-top-btn.visible {
opacity: 1;
visibility: visible;
transform: translateY(0);
}
.scroll-top-btn.hidden {
opacity: 0;
visibility: hidden;
transform: translateY(10px);
}
.back-btn {
bottom: 40px;
right: 21.7%;
transition: transform 0.4s ease;
}
.back-btn.shifted {
transform: translateY(-50px);
}
/* Scroll Button end */
/* 중앙 콘텐츠 자동 조정 */
.layout-page {
flex-grow: 1;
min-width: 0; /* flexbox 내에서 올바른 크기 계산 */
margin-right: 20%; /* 채팅 사이드바의 너비만큼 밀리도록 설정 */
}
/* 중앙 콘텐츠 자동 조정 end */
/* media */
/* 탑바 범위조정 */
.layout-navbar-fixed .layout-navbar.navbar-detached {
width: calc(100% - 1.625rem * 2 - 16.25rem - 20%) !important;
}
@media (min-width: 1200px) {
nav#layout-navbar {
left: calc(16.25rem - 20%) !important;
}
.layout-navbar.navbar-detached.container-xxl {
max-width: calc(1440px - 1.625rem * 2) !important;
}
}
/* 탑바 범위조정(1200px 이하) */
@media (max-width: 1200px) {
nav#layout-navbar {
left: -20% !important;
}
.layout-navbar-fixed .layout-navbar.navbar-detached {
width: calc(100% - 1.625rem * 2 - 20%) !important;
}
}
/* 탑바 범위조정(992px 이하) */
@media (max-width: 992px) {
.layout-navbar-fixed .layout-navbar.navbar-detached {
width: calc(100% - 2rem - 20%) !important;
}
}
/* Mobile */
/* 가로모드 모바일 디바이스 (가로 해상도가 768px 보다 작은 화면에 적용) */
@media (max-width: 767px) {
.chat-sidebar {
display: none !important;
}
nav#layout-navbar {
left: 0 !important;
}
.layout-navbar-fixed .layout-navbar.navbar-detached {
width: calc(100% - 2rem) !important;
}
.layout-page {
margin-right: 0 !important;
}
#app-calendar-sidebar {
width: 100%;
}
}
/* 세로모드 모바일 디바이스 (가로 해상도가 576px 보다 작은 화면에 적용) */
@media (max-width: 575px) {
}
/* Mobile end */
/* media end */
/* BoardComment */
.beforeRotate {
transition: transform 0.3s ease-in-out;
}
.rotate {
transform: rotate(45deg);
transition: transform 0.3s ease-in-out;
}
/* BoardComment end */
/* vote */
.hidden-date-input {
position: absolute;
top: 31.5%;
left: 17%;
width: 100%;
opacity: 0;
pointer-events: none;
}
.hidden-time-input {
position: absolute;
opacity: 0;
pointer-events: none;
}
/* 권한부여 */
.user-card-container {
display: flex;
flex-wrap: wrap;
gap: 20px;
justify-content: center;
margin-top: 20px;
}
.user-card {
width: 200px;
padding: 15px;
border: 1px solid #ddd;
border-radius: 10px;
background-color: #fff;
box-shadow: 2px 2px 10px rgba(0, 0, 0, 0.1);
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
}
.profile-img {
width: 80px;
height: 80px;
border-radius: 50%;
object-fit: cover;
margin-bottom: 10px;
}
.switch {
position: relative;
display: inline-block;
width: 50px;
height: 24px;
margin-top: 5px;
}
.switch input {
opacity: 0;
width: 0;
height: 0;
}
.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #ccc;
transition: 0.4s;
border-radius: 24px;
}
.slider:before {
position: absolute;
content: '';
height: 18px;
width: 18px;
left: 4px;
bottom: 3px;
background-color: white;
transition: 0.4s;
border-radius: 50%;
}
input:checked + .slider {
background-color: #4caf50;
}
input:checked + .slider:before {
transform: translateX(24px);
}
/* 권한부여 끝 */
/* toast */
.bs-toast {
bottom: 20px;
right: 20px;
}
/* toast end */
.cursor-none {
cursor: none !important;
}
.ml-1 {
margin-left: 0.25rem !important;
}
.mr-1 {
margin-right: 0.25rem !important;
}
.nickname-ellipsis {
white-space: nowrap;
max-width: 100px;
vertical-align: middle;
}
/* font css */
.font-bold {
font-weight: bold;
}
.pointer {
cursor: pointer;
} }

View File

@ -1,21 +1,163 @@
/* 1) */
@font-face {
font-family: 'NanumSquareRound';
src: url('https://cdn.jsdelivr.net/gh/projectnoonnu/noonfonts_two@1.0/NanumSquareRound.woff') format('woff');
font-weight: normal;
font-style: normal;
}
body {
font-family: 'NanumSquareRound', sans-serif;
font-size: 16px;
line-height: 1.6;
color: #333;
}
/* 2) */
/* @font-face {
font-family: 'Pretendard-Regular';
src: url('https://cdn.jsdelivr.net/gh/Project-Noonnu/noonfonts_2107@1.1/Pretendard-Regular.woff') format('woff');
font-weight: 400;
font-style: normal;
}
body {
font-family: 'Pretendard-Regular', sans-serif;
font-size: 16px;
line-height: 1.6;
color: #333;
} */
/* 3) */
/* @font-face {
font-family: 'NEXON Lv1 Gothic OTF';
src: url('https://fastly.jsdelivr.net/gh/projectnoonnu/noonfonts_20-04@2.1/NEXON Lv1 Gothic OTF.woff') format('woff');
font-weight: normal;
font-style: normal;
}
body {
font-family: 'NEXON Lv1 Gothic OTF', sans-serif;
font-size: 16px;
line-height: 1.6;
color: #333;
} */
/* 4) */
/* @font-face {
font-family: 'SUITE-Regular';
src: url('https://fastly.jsdelivr.net/gh/projectnoonnu/noonfonts_2304-2@1.0/SUITE-Regular.woff2') format('woff2');
font-weight: 400;
font-style: normal;
}
body {
font-family: 'SUITE-Regular', sans-serif;
font-size: 16px;
line-height: 1.6;
color: #333;
} */
/* 5) */
/* @font-face {
font-family: 'GoyangIlsan';
src: url('https://fastly.jsdelivr.net/gh/projectnoonnu/noonfonts_one@1.0/GoyangIlsan.woff') format('woff');
font-weight: normal;
font-style: normal;
}
body {
font-family: 'GoyangIlsan', sans-serif;
font-size: 16px;
line-height: 1.6;
color: #333;
} */
/* 6) */
/* @font-face {
font-family: 'GowunDodum-Regular';
src: url('https://fastly.jsdelivr.net/gh/projectnoonnu/noonfonts_2108@1.1/GowunDodum-Regular.woff') format('woff');
font-weight: normal;
font-style: normal;
}
body {
font-family: 'GowunDodum-Regular', sans-serif;
font-size: 16px;
line-height: 1.6;
color: #333;
} */
/* 7) */
/* @font-face {
font-family: 'EASTARJET-Medium';
src: url('https://fastly.jsdelivr.net/gh/projectnoonnu/noonfonts_231029@1.1/EASTARJET-Medium.woff2') format('woff2');
font-weight: 500;
font-style: normal;
}
body {
font-family: 'EASTARJET-Medium', sans-serif;
font-size: 16px;
line-height: 1.6;
color: #333;
} */
/* 8) */
/* @font-face {
font-family: 'HakgyoansimChulseokbuTTF-B';
src: url('https://fastly.jsdelivr.net/gh/projectnoonnu/2408-5@1.0/HakgyoansimChulseokbuTTF-B.woff2') format('woff2');
font-weight: 400;
font-style: normal;
}
body {
font-family: 'HakgyoansimChulseokbuTTF-B', sans-serif;
font-size: 16px;
line-height: 1.6;
color: #333;
} */
/* 9) */
/* @font-face {
font-family: 'GongGothicMedium';
src: url('https://fastly.jsdelivr.net/gh/projectnoonnu/noonfonts_20-10@1.0/GongGothicMedium.woff') format('woff');
font-weight: normal;
font-style: normal;
}
body {
font-family: 'GongGothicMedium', sans-serif;
font-size: 16px;
line-height: 1.6;
color: #333;
} */
/* @font-face {
font-family: 'MangoDdobak-B';
src: url('https://fastly.jsdelivr.net/gh/projectnoonnu/2405-3@1.1/MangoDdobak-B.woff2') format('woff2');
font-weight: 700;
font-style: normal;
}
body {
font-family: 'MangoDdobak-B', sans-serif;
font-size: 16px;
line-height: 1.6;
color: #333;
} */
/* 나눔고딕 */ /* 나눔고딕 */
@import url('https://fonts.googleapis.com/css2?family=Nanum+Gothic&display=swap'); @import url('https://fonts.googleapis.com/css2?family=Nanum+Gothic&display=swap');
/* D2Coding 폰트 */
@font-face { @font-face {
font-family: 'D2Coding'; font-family: 'D2Coding';
src: url('/font/D2Coding-Ver1.3.2-20180524-all.ttc') format('ttc'); src: url('/font/D2Coding-Ver1.3.2-20180524-all.ttc') format('truetype');
font-weight: normal; font-weight: normal;
font-style: normal; font-style: normal;
} }
/* Consolas 폰트 */
@font-face { @font-face {
font-family: 'Consolas'; font-family: 'Consolas';
src: url('/font/Consolas.woff') format('woff'); src: url('/font/Consolas.woff') format('font-woff');
font-weight: normal; font-weight: normal;
font-style: normal; font-style: normal;
} }
/* 툴바에서 선택 가능한 폰트 및 크기 스타일 */ /* 툴바에서 선택 가능한 폰트 */
.ql-font-nanum-gothic { .ql-font-nanum-gothic {
font-family: 'Nanum Gothic', sans-serif; font-family: 'Nanum Gothic', sans-serif;
} }
@ -33,24 +175,10 @@
} }
/* 폰트 크기 스타일 */ /* 폰트 크기 스타일 */
.ql-size-12px { .ql-size-12px { font-size: 12px; }
font-size: 12px; .ql-size-14px { font-size: 14px; }
} .ql-size-16px { font-size: 16px; }
.ql-size-14px { .ql-size-18px { font-size: 18px; }
font-size: 14px; .ql-size-24px { font-size: 24px; }
} .ql-size-32px { font-size: 32px; }
.ql-size-16px { .ql-size-48px { font-size: 48px; }
font-size: 16px;
}
.ql-size-18px {
font-size: 18px;
}
.ql-size-24px {
font-size: 24px;
}
.ql-size-32px {
font-size: 32px;
}
.ql-size-48px {
font-size: 48px;
}

BIN
public/img/icons/Crown.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 229 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 212 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

BIN
public/img/mbti/enfj.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

BIN
public/img/mbti/enfp.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
public/img/mbti/entj.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

BIN
public/img/mbti/entp.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

BIN
public/img/mbti/esfj.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
public/img/mbti/esfp.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

BIN
public/img/mbti/estj.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

BIN
public/img/mbti/estp.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

BIN
public/img/mbti/infj.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
public/img/mbti/infp.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

BIN
public/img/mbti/intj.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

BIN
public/img/mbti/intp.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
public/img/mbti/isfj.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

BIN
public/img/mbti/isfp.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
public/img/mbti/istj.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

BIN
public/img/mbti/istp.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View File

@ -2,116 +2,116 @@
* Main * Main
*/ */
'use strict' (function () {
let menu, animate
;(function () {
// Initialize menu // Initialize menu
//----------------- //-----------------
let menu, animate;
let layoutMenuEl = document.querySelectorAll('#layout-menu') let layoutMenuEl = document.querySelectorAll('#layout-menu');
layoutMenuEl.forEach(function (element) { layoutMenuEl.forEach(function (element) {
menu = new Menu(element, { menu = new Menu(element, {
orientation: 'vertical', orientation: 'vertical',
closeChildren: false, closeChildren: false,
}) });
// Change parameter to true if you want scroll animation // Change parameter to true if you want scroll animation
window.Helpers.scrollToActive((animate = false)) window.Helpers.scrollToActive((animate = false));
window.Helpers.mainMenu = menu window.Helpers.mainMenu = menu;
}) });
// Initialize menu togglers and bind click on each // Initialize menu togglers and bind click on each
let menuToggler = document.querySelectorAll('.layout-menu-toggle') let menuToggler = document.querySelectorAll('.layout-menu-toggle');
menuToggler.forEach((item) => { menuToggler.forEach(item => {
item.addEventListener('click', (event) => { item.addEventListener('click', event => {
event.preventDefault() event.preventDefault();
window.Helpers.toggleCollapsed() window.Helpers.toggleCollapsed();
}) });
}) });
// Display menu toggle (layout-menu-toggle) on hover with delay // Display menu toggle (layout-menu-toggle) on hover with delay
let delay = function (elem, callback) { let delay = function (elem, callback) {
let timeout = null let timeout = null;
elem.onmouseenter = function () { elem.onmouseenter = function () {
// Set timeout to be a timer which will invoke callback after 300ms (not for small screen) // Set timeout to be a timer which will invoke callback after 300ms (not for small screen)
if (!Helpers.isSmallScreen()) { if (!Helpers.isSmallScreen()) {
timeout = setTimeout(callback, 300) timeout = setTimeout(callback, 300);
} else { } else {
timeout = setTimeout(callback, 0) timeout = setTimeout(callback, 0);
}
} }
};
elem.onmouseleave = function () { elem.onmouseleave = function () {
// Clear any timers set to timeout // Clear any timers set to timeout
document.querySelector('.layout-menu-toggle').classList.remove('d-block') document.querySelector('.layout-menu-toggle').classList.remove('d-block');
clearTimeout(timeout) clearTimeout(timeout);
} };
} };
if (document.getElementById('layout-menu')) { if (document.getElementById('layout-menu')) {
delay(document.getElementById('layout-menu'), function () { delay(document.getElementById('layout-menu'), function () {
// not for small screen // not for small screen
if (!Helpers.isSmallScreen()) { if (!Helpers.isSmallScreen()) {
document.querySelector('.layout-menu-toggle').classList.add('d-block') document.querySelector('.layout-menu-toggle').classList.add('d-block');
} }
}) });
} }
// Display in main menu when menu scrolls // Display in main menu when menu scrolls
let menuInnerContainer = document.getElementsByClassName('menu-inner'), let menuInnerContainer = document.getElementsByClassName('menu-inner'),
menuInnerShadow = document.getElementsByClassName('menu-inner-shadow')[0] menuInnerShadow = document.getElementsByClassName('menu-inner-shadow')[0];
if (menuInnerContainer.length > 0 && menuInnerShadow) { if (menuInnerContainer.length > 0 && menuInnerShadow) {
menuInnerContainer[0].addEventListener('ps-scroll-y', function () { menuInnerContainer[0].addEventListener('ps-scroll-y', function () {
if (this.querySelector('.ps__thumb-y').offsetTop) { if (this.querySelector('.ps__thumb-y').offsetTop) {
menuInnerShadow.style.display = 'block' menuInnerShadow.style.display = 'block';
} else { } else {
menuInnerShadow.style.display = 'none' menuInnerShadow.style.display = 'none';
} }
}) });
} }
// Init helpers & misc // Init helpers & misc
// -------------------- // --------------------
// Init BS Tooltip // Init BS Tooltip
const tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]')) const tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
tooltipTriggerList.map(function (tooltipTriggerEl) { tooltipTriggerList.map(function (tooltipTriggerEl) {
return new bootstrap.Tooltip(tooltipTriggerEl) return new bootstrap.Tooltip(tooltipTriggerEl);
}) });
// Accordion active class // Accordion active class
const accordionActiveFunction = function (e) { const accordionActiveFunction = function (e) {
if (e.type == 'show.bs.collapse' || e.type == 'show.bs.collapse') { if (e.type == 'show.bs.collapse' || e.type == 'show.bs.collapse') {
e.target.closest('.accordion-item').classList.add('active') e.target.closest('.accordion-item').classList.add('active');
} else { } else {
e.target.closest('.accordion-item').classList.remove('active') e.target.closest('.accordion-item').classList.remove('active');
}
} }
};
const accordionTriggerList = [].slice.call(document.querySelectorAll('.accordion')) const accordionTriggerList = [].slice.call(document.querySelectorAll('.accordion'));
const accordionList = accordionTriggerList.map(function (accordionTriggerEl) { const accordionList = accordionTriggerList.map(function (accordionTriggerEl) {
accordionTriggerEl.addEventListener('show.bs.collapse', accordionActiveFunction) accordionTriggerEl.addEventListener('show.bs.collapse', accordionActiveFunction);
accordionTriggerEl.addEventListener('hide.bs.collapse', accordionActiveFunction) accordionTriggerEl.addEventListener('hide.bs.collapse', accordionActiveFunction);
}) });
// Auto update layout based on screen size // Auto update layout based on screen size
window.Helpers.setAutoUpdate(true) window.Helpers.setAutoUpdate(true);
// Toggle Password Visibility // Toggle Password Visibility
window.Helpers.initPasswordToggle() window.Helpers.initPasswordToggle();
// Speech To Text // Speech To Text
window.Helpers.initSpeechToText() window.Helpers.initSpeechToText();
// Manage menu expanded/collapsed with templateCustomizer & local storage // Manage menu expanded/collapsed with templateCustomizer & local storage
//------------------------------------------------------------------ //------------------------------------------------------------------
// If current layout is horizontal OR current window screen is small (overlay menu) than return from here // If current layout is horizontal OR current window screen is small (overlay menu) than return from here
if (window.Helpers.isSmallScreen()) { if (window.Helpers.isSmallScreen()) {
return return;
} }
// If current layout is vertical and current window screen is > small // If current layout is vertical and current window screen is > small
// Auto update menu collapsed/expanded based on the themeConfig // Auto update menu collapsed/expanded based on the themeConfig
window.Helpers.setCollapsed(true, false) // 250304 pc 화면에서 메뉴바 고정을 위해 false 처리
})() window.Helpers.setCollapsed(false, false);
//window.Helpers.setCollapsed(true, false);
})();

View File

@ -15304,12 +15304,12 @@ html:not(.layout-menu-fixed) .menu-inner-shadow {
} }
@media (max-width: 1199.98px) { @media (max-width: 1199.98px) {
.layout-navbar-fixed .layout-navbar.navbar-detached { .layout-navbar-fixed .layout-navbar.navbar-detached {
width: calc(100% - (1.625rem * 2)) !important; width: calc(100% - (1.625rem * 2))
} }
} }
@media (max-width: 991.98px) { @media (max-width: 991.98px) {
.layout-navbar-fixed .layout-navbar.navbar-detached { .layout-navbar-fixed .layout-navbar.navbar-detached {
width: calc(100% - (1rem * 2)) !important; width: calc(100% - (1rem * 2))
} }
} }
.layout-navbar-fixed.layout-menu-collapsed .layout-navbar.navbar-detached { .layout-navbar-fixed.layout-menu-collapsed .layout-navbar.navbar-detached {
@ -15317,12 +15317,12 @@ html:not(.layout-menu-fixed) .menu-inner-shadow {
} }
@media (max-width: 1199.98px) { @media (max-width: 1199.98px) {
.layout-navbar.navbar-detached { .layout-navbar.navbar-detached {
width: calc(100vw - (100vw - 100%) - (1.625rem * 2)) !important; width: calc(100vw - (100vw - 100%) - (1.625rem * 2))
} }
} }
@media (max-width: 991.98px) { @media (max-width: 991.98px) {
.layout-navbar.navbar-detached { .layout-navbar.navbar-detached {
width: calc(100vw - (100vw - 100%) - (1rem * 2)) !important; width: calc(100vw - (100vw - 100%) - (1rem * 2))
} }
} }
.layout-menu-collapsed .layout-navbar.navbar-detached, .layout-without-menu .layout-navbar.navbar-detached { .layout-menu-collapsed .layout-navbar.navbar-detached, .layout-without-menu .layout-navbar.navbar-detached {

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,6 +1,8 @@
<template> <template>
<link rel="stylesheet" href="/css/font.css">
<component :is="layout"> <component :is="layout">
<template #content> <template #content>
<LoadingSpinner :isLoading="loadingStore.isLoading" />
<router-view></router-view> <router-view></router-view>
</template> </template>
</component> </component>
@ -12,7 +14,10 @@ import { useRoute } from 'vue-router';
import NormalLayout from './layouts/NormalLayout.vue'; import NormalLayout from './layouts/NormalLayout.vue';
import NoLayout from './layouts/NoLayout.vue'; import NoLayout from './layouts/NoLayout.vue';
import ToastModal from '@c/modal/ToastModal.vue'; import ToastModal from '@c/modal/ToastModal.vue';
import { useLoadingStore } from "@s/loadingStore";
import LoadingSpinner from "@v/LoadingPage.vue";
const loadingStore = useLoadingStore();
const route = useRoute(); const route = useRoute();
const layout = computed(() => { const layout = computed(() => {

View File

@ -1,11 +1,13 @@
import axios from "axios"; import axios from 'axios';
import router from "@/router/index"; import router from '@/router';
import { useToastStore } from '@s/toastStore';
import { useLoadingStore } from '@s/loadingStore';
const $api = axios.create({ const $api = axios.create({
baseURL: 'http://localhost:10325/api/', baseURL: import.meta.env.VITE_API_URL,
timeout: 300000, timeout: 300000,
withCredentials : true withCredentials: true,
}) });
/** /**
* Default Content-Type : json * Default Content-Type : json
@ -13,6 +15,8 @@ const $api = axios.create({
*/ */
$api.interceptors.request.use( $api.interceptors.request.use(
function (config) { function (config) {
const loadingStore = useLoadingStore();
loadingStore.startLoading();
let contentType = 'application/json'; let contentType = 'application/json';
@ -22,21 +26,98 @@ $api.interceptors.request.use(
config.headers['X-Requested-With'] = 'XMLHttpRequest'; config.headers['X-Requested-With'] = 'XMLHttpRequest';
return config; return config;
}, function (error) { },
function (error) {
const loadingStore = useLoadingStore();
loadingStore.stopLoading();
// 요청 오류가 있는 작업 수행 // 요청 오류가 있는 작업 수행
return Promise.reject(error); return Promise.reject(error);
} },
); );
// 응답 인터셉터 추가하기 // 응답 인터셉터 추가하기
$api.interceptors.response.use( $api.interceptors.response.use(
function (response) { function (response) {
// 2xx 범위에 있는 상태 코드는 이 함수를 트리거 합니다. const loadingStore = useLoadingStore();
// 응답 데이터가 있는 작업 수행 loadingStore.stopLoading();
// 로그인 요청일 경우 (헤더에 isLogin이 true로 설정된 경우)
if (response.config.headers && response.config.headers.isLogin) {
return response; return response;
}, function (error) { }
// 2xx 외의 범위에 있는 상태 코드는 이 함수를 트리거 합니다.
// 응답 오류가 있는 작업 수행 // 테스트 부탁
// 로그인 실패, 커스텀 에러 응답 처리
if (response.data.code > 10000) {
const toastStore = useToastStore();
const errorCode = response.data.code;
const errorMessage = response.data.message || '알 수 없는 오류가 발생했습니다.';
// 서버에서 보낸 메시지 사용
toastStore.onToast(errorMessage, 'e');
// 특정 에러 코드에 대한 추가 처리만 수행
if (errorCode === 10001) {
router.push('/login');
}
// 오류 응답 반환
return response;
}
// 일반 성공 응답 처리
return response;
},
function (error) {
const loadingStore = useLoadingStore();
loadingStore.stopLoading();
const toastStore = useToastStore();
// 로그인 요청 별도 처리 (헤더에 isLogin이 true로 설정된 경우)
if (error.config && error.config.headers && error.config.headers.isLogin) {
// 로그인 페이지 오류 토스트 메시지 표시 X
return Promise.reject(error); return Promise.reject(error);
}); }
// 에러 응답에 커스텀 메시지가 포함되어 있다면 해당 메시지 사용
// if (error.response && error.response.data && error.response.data.message) {
// toastStore.onToast(error.response.data.message, 'e');
// } else if (error.response) {
if (error.response) {
// 기본 HTTP 에러 처리
switch (error.response.status) {
case 400:
toastStore.onToast('잘못된 요청입니다.', 'e');
router.push('/error/400');
break;
case 401:
toastStore.onToast('인증이 필요합니다.', 'e');
router.push('/login');
break;
case 403:
toastStore.onToast('접근 권한이 없습니다.', 'e');
break;
case 404:
toastStore.onToast('요청한 페이지를 찾을 수 없습니다.', 'e');
router.push('/error/404');
break;
case 500:
toastStore.onToast('서버 오류가 발생했습니다.', 'e');
router.push('/error/500');
break;
default:
toastStore.onToast('알 수 없는 오류가 발생했습니다.', 'e');
}
} else if (error.request) {
toastStore.onToast('서버와 통신할 수 없습니다.', 'e');
} else {
toastStore.onToast('요청 중 오류가 발생했습니다.', 'e');
}
return Promise.reject(error);
},
);
export default $api; export default $api;

View File

@ -1,8 +1,8 @@
/* /*
작성자 : 공현지 작성자 : 공현지
작성일 : 2025-01-17 작성일 : 2025-01-17
수정자 : 수정자 : 박성용
수정일 : 수정일 : 2025-03-11
설명 : 공통 스크립트 설명 : 공통 스크립트
*/ */
import Quill from 'quill'; import Quill from 'quill';
@ -12,7 +12,9 @@ import Quill from 'quill';
*setup() 사용법 : *setup() 사용법 :
const { appContext } = getCurrentInstance(); const { appContext } = getCurrentInstance();
const $common = appContext.config.globalProperties.$common; const $common = appContext.config.globalProperties.$common;
$common.변수 or
import { inject } from 'vue';
const $common = inject('common');
*/ */
const common = { const common = {
// JSON 문자열로 Delta 타입을 변환 // JSON 문자열로 Delta 타입을 변환
@ -38,12 +40,182 @@ const common = {
} }
console.error('잘못된 Delta 객체:', content); console.error('잘못된 Delta 객체:', content);
return null; // Delta 객체가 아니거나 ops가 없을 경우 null 반환 return null; // Delta 객체가 아니거나 ops가 없을 경우 null 반환
},
/**
* Date 타입 문자열 포멧팅
*
* @param {string} dateStr
* @return
* 1. Date type 경우 예시 '2025-02-24 12:02'
* 2. Date type 아닌 경우 입력값 리턴
*
*/
dateFormatter(dateStr, type = null) {
const date = new Date(dateStr);
const dateCheck = date.getTime();
if (isNaN(dateCheck)) {
return dateStr;
} else {
const { year, month, day, hours, minutes } = this.formatDateTime(date);
let callback = '';
if (type == 'YMD') {
callback = `${year}-${month}-${day}`;
} else if (type == 'MD') {
callback = `${month}-${day}`;
} else if (type == 'T') {
callback = `${hours}:${minutes}`;
} else {
callback = `${year}-${month}-${day} ${hours}:${minutes}`;
} }
return callback;
} }
},
formatDateTime(dateStr) {
const date = new Date(dateStr);
const dateCheck = date.getTime();
if (isNaN(dateCheck)) return dateStr;
const zeroFormat = num => (num < 10 ? `0${num}` : num);
return {
year: date.getFullYear(),
month: zeroFormat(date.getMonth() + 1),
day: zeroFormat(date.getDate()),
hours: zeroFormat(date.getHours()),
minutes: zeroFormat(date.getMinutes()),
seconds: zeroFormat(date.getSeconds()),
};
},
// 오늘 날짜시간 조회
getToday() {
const date = new Date();
return {
year: date.getFullYear(),
month: date.getMonth() + 1,
day: date.getDate(),
hours: date.getHours(),
minutes: date.getMinutes(),
seconds: date.getSeconds(),
};
},
// 해당 날짜가 오늘인지 확인
isToday(dateStr) {
const date = new Date(dateStr);
const dateCheck = date.getTime();
if (isNaN(dateCheck)) return '날짜 타입 에러';
const today = new Date();
return date.toDateString() === today.toDateString();
},
// 해당 월, 일에 맞는 목록 필터링
filterTargetByDate(target, key, month, day) {
if (!Array.isArray(target) || target.length === 0) return [];
return [...target].filter(item => {
if (!item[key]) return false;
const date = new Date(item[key]);
const MatchingMonth = date.getMonth() + 1 === parseInt(month, 10);
const MatchingDay = date.getDate() === parseInt(day, 10);
return MatchingMonth && MatchingDay;
});
},
/**
* 빈값 확인
*
* @param {} obj
* @returns
*/
isNotEmpty(obj) {
if (obj === null || obj === undefined) {
return false;
}
if (typeof obj === 'string' && obj.trim() === '') {
return false;
}
if ((Array.isArray(obj) || obj === Object(obj)) && Object.keys(obj).length === 0) {
return false;
}
return true;
},
/**
* 에디터에 내용이 있는지 확인
*
* @param { Quill } content
* @returns true: 없음, false: 있음
*/
isNotValidContent(content) {
if (!content.value?.ops?.length) return true;
// 이미지 포함 여부 확인
const hasImage = content.value.ops.some(op => op.insert && typeof op.insert === 'object' && op.insert.image);
// 텍스트 포함 여부 확인
const hasText = content.value.ops.some(op => typeof op.insert === 'string' && op.insert.trim().length > 0);
// 텍스트 또는 이미지가 하나라도 있으면 유효한 내용
return !(hasText || hasImage);
},
/**
* 확인
*
* @param { ref } text ex) inNotValidInput(data.value);
* @returns
*/
isNotValidInput(text) {
return text.trim().length === 0;
},
/**
* 프로필 이미지 반환
*
* @param { String } profileImg
* @returns
*/
getProfileImage(profileImg, isAnonymous = false) {
const defaultProfileImg = '/img/icons/icon.png'; // 기본 프로필 이미지 경로
const anonymousImg = '/img/avatars/default-Profile.jpg'; // 익명 이미지
let profileImgUrl = isAnonymous ? anonymousImg : defaultProfileImg;
const UserProfile = `${import.meta.env.VITE_SERVER}upload/img/profile/${profileImg}`;
return !profileImg || profileImg === '' ? profileImgUrl : UserProfile;
},
setDefaultImage(event, deafultImg = '/img/icons/icon.png') {
return (event.target.src = deafultImg);
},
showImage(event) {
return (event.target.style.visibility = 'visible');
},
addHyphenToPhoneNumber(phoneNum) {
const phoneNumber = phoneNum;
const length = phoneNumber.length;
if (length >= 9) {
return phoneNumber.replace(/[^0-9]/g, '').replace(/^(\d{2,3})(\d{3,4})(\d{4})$/, `$1-$2-$3`);
} else {
return phoneNum;
}
},
};
export default { export default {
install(app) { install(app) {
app.config.globalProperties.$common = common; app.config.globalProperties.$common = common;
} app.provide('common', common);
},
}; };

67
src/common/commonApi.js Normal file
View File

@ -0,0 +1,67 @@
/*
작성자 : 박지윤
작성일 : 2025-02-04
수정자 :
수정일 :
설명 : 공통 api
*/
import { ref, onMounted } from "vue";
import $api from '@api';
const colorList = ref([]);
const mbtiList = ref([]);
const pwhintList = ref([]);
const yearCategory = ref([]);
const cateList = ref([]);
const refreshColorList = async (type = 'YNP') => {
const response = await $api.get(`user/color`, {
params: { type }
});
if (response.data && response.data.data) {
colorList.value = response.data.data.map(item => ({
label: item.CMNCODNAM,
value: item.CMNCODVAL,
}));
}
return colorList.value;
};
// CommonCode 함수를 외부에서도 접근할 수 있게 변경
const CommonCode = async (path, endpoint, targetList, type = null) => {
const params = type ? { type } : {};
const response = await $api.get(`${path}/${endpoint}`, {
params
});
targetList.value = response.data.data.map(item => ({
label: item.CMNCODNAM,
value: item.CMNCODVAL,
}));
};
const commonApi = (options = {}) => {
onMounted(async () => {
// 요청할 데이터가 옵션으로 전달 -> 그에 맞게 호출
if (options.loadColor) {
await CommonCode("user", "color", colorList, options.colorType);
}
if (options.loadMbti) await CommonCode("user", "mbti", mbtiList);
if (options.loadPwhint) await CommonCode("user", "pwhint", pwhintList);
if (options.loadYearCategory) await CommonCode("project", "yearCategory", yearCategory);
if (options.loadCateList) await CommonCode("worddict", "getWordCategory", cateList);
});
return {
colorList,
mbtiList,
pwhintList,
yearCategory,
cateList,
refreshColorList
};
};
export { refreshColorList };
export default commonApi;

View File

@ -0,0 +1,13 @@
/** 날짜 포맷1 (YYYY-MM-DD HH:MM) */
export const formattedDate = (dateString) => {
if (!dateString) return "날짜 없음";
const dateObj = new Date(dateString);
return `${dateObj.getFullYear()}-${String(dateObj.getMonth() + 1).padStart(2, '0')}-${String(dateObj.getDate()).padStart(2, '0')} ${String(dateObj.getHours()).padStart(2, '0')}:${String(dateObj.getMinutes()).padStart(2, '0')}`;
};
/** 날짜 포맷2 (YYYY-MM-DD) */
export const formatDate = (dateString) => {
if (!dateString) return "날짜 없음";
const dateObj = new Date(dateString);
return `${dateObj.getFullYear()}-${String(dateObj.getMonth() + 1).padStart(2, '0')}-${String(dateObj.getDate()).padStart(2, '0')}`;
};

View File

@ -1,62 +0,0 @@
<template>
<div class="card shadow-none border mt-6">
<div class="card-body">
<!-- 댓글 입력 섹션 -->
<div class="d-flex justify-content-start align-items-top">
<!-- 프로필섹션 -->
<div class="avatar-wrapper">
<div class="avatar me-4">
<img src="/img/avatars/11.png" alt="Avatar" class="rounded-circle">
</div>
</div>
<!-- 텍스트박스 -->
<div class="w-100">
<textarea
class="form-control"
placeholder="주제에 대한 생각을 자유롭게 댓글로 표현해 주세요. &#13;&#10;여러분의 다양한 의견을 기다립니다."
rows="3"
></textarea>
</div>
</div>
<!-- 옵션 버튼 섹션 -->
<div class="d-flex justify-content-between flex-wrap mt-4">
<div class="d-flex flex-wrap align-items-center">
<!-- 익명 체크박스 (익명게시판일 경우에만)-->
<div class="form-check form-check-inline mb-0 me-4">
<input
class="form-check-input"
type="checkbox"
id="inlineCheckbox1"
v-model="isCheck"
/>
<label class="form-check-label" for="inlineCheckbox1">익명</label>
</div>
<!-- 비밀번호 입력 필드 (익명이 선택된 경우에만 표시) -->
<div v-if="isCheck" class="d-flex align-items-center flex-grow-1">
<label class="form-label mb-0 me-3" for="basic-default-password">비밀번호</label>
<input
type="password"
id="basic-default-password"
class="form-control flex-grow-1"
placeholder=""
/>
</div>
</div>
<!-- 답변 쓰기 버튼 -->
<div class="ms-auto mt-3 mt-md-0">
<button class="btn btn-primary">
<i class="icon-base bx bx-check"></i>
</button>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue';
const isCheck = ref(false);
</script>

View File

@ -1,56 +1,162 @@
<template> <template>
<div> <div>
<BoardProfile :profileName="comment.author" :showDetail="false" :author="true" /> <BoardProfile
<div class="my-6"> :unknown="comment.author === '익명'"
<p>{{ comment.content }}</p> :isCommentAuthor="isCommentAuthor"
:boardId="comment.boardId"
:profileName="displayName"
:date="comment.createdAt"
:comment="comment"
:profileImg="comment.profileImg"
:showDetail="false"
:isLike="!isLike"
:isCommentPassword="isCommentPassword"
:isCommentProfile="true"
:is-edit-pushed="isEditPushed"
:is-delete-pushed="isDeletePushed"
@editClick="handleEditClick"
@deleteClick="$emit('deleteClick', comment)"
@updateReaction="handleUpdateReaction"
/>
<!-- 댓글 비밀번호 입력창 (익명일 경우) -->
<div v-if="currentPasswordCommentId === comment.commentId && unknown && comment.author == '익명'" class="mt-3 w-px-200 ms-auto">
<div class="input-group">
<input
type="password"
class="form-control"
:value="password"
autocomplete="new-password"
maxlength="8"
placeholder="비밀번호 입력"
@input="filterInput"
/>
<button class="btn btn-primary" @click="logPasswordAndEmit">확인</button>
</div>
<span v-if="passwordCommentAlert" class="invalid-feedback d-block text-start">{{ passwordCommentAlert }}</span>
</div> </div>
<PlusButton v-if="isPlusButton" @click="toggleComment"/>
<BoardComentArea v-if="isComment" @submit="submitComment"/>
<!-- 대댓글 --> <div class="mt-6">
<ul v-if="comment.children && comment.children.length" class="list-unstyled"> <template v-if="comment.isEditTextarea">
<li <textarea v-model="localEditedContent" class="form-control" maxLength="500"></textarea>
v-for="child in comment.children" <span v-if="editCommentAlert" class="invalid-feedback d-block text-start">{{ editCommentAlert }}</span>
:key="child.id" <div class="mt-2 d-flex justify-content-end">
class="pt-6 ps-10" <SaveBtn class="btn btn-primary" @click="submitEdit" :isEnabled="disabled"></SaveBtn>
> </div>
<BoardComment :comment="child" :isPlusButton="false" @submitComment="addChildComment" /> </template>
</li>
</ul> <template v-else>
<!-- <ul class="list-unstyled twoDepth"> <div class="m-0" style="white-space: pre-wrap">{{ comment.content }}</div>
<li> </template>
<BoardProfile profileName=곤데리2 :showDetail="false" /> </div>
<div class="mt-2">저도 궁금합니다.</div> <!-- <p>현재 isDeleted : {{ isDeleted }}</p> -->
<BoardComentArea v-if="comment" />
</li> <!-- <template v-if="isDeleted">
</ul> --> <p class="m-0 text-muted">댓글이 삭제되었습니다.</p>
<!-- <BoardProfile profileName=곤데리 :showDetail="false" /> </template> -->
<div class="mt-2">저도 궁금합니다.</div> <PlusButton v-if="isPlusButton" @click="toggleComment" class="mt-6">
<PlusButton @click="toggleComment"/> <i class="icon-base bx bx-plus beforeRotate" :class="{ rotate: isComment }"></i>
<BoardComentArea v-if="comment" /> --> </PlusButton>
<BoardCommentArea v-if="isComment" :unknown="unknown" @submitComment="submitComment" :commnetId="comment.commentId" />
<slot name="reply"></slot>
</div> </div>
</template> </template>
<script setup> <script setup>
import { defineProps, defineEmits, ref, computed, watch, inject } from 'vue';
import BoardProfile from './BoardProfile.vue'; import BoardProfile from './BoardProfile.vue';
import BoardComentArea from './BoardComentArea.vue'; import BoardCommentArea from './BoardCommentArea.vue';
import PlusButton from '../button/PlusBtn.vue'; import PlusButton from '../button/PlusBtn.vue';
import { ref } from 'vue'; import SaveBtn from '../button/SaveBtn.vue';
import { defineEmits } from 'vue';
const props = defineProps({ const props = defineProps({
comment: { comment: {
type: Object, type: Object,
required: true, required: true,
}, },
unknown: {
type: Boolean,
default: false,
},
nickname: {
type: String,
default: '',
},
isCommentAuthor: {
type: Boolean,
default: false,
},
isPlusButton: { isPlusButton: {
type: Boolean, type: Boolean,
default: true, default: true,
} },
isLike: {
type: Boolean,
default: false,
},
isEditTextarea: {
type: Boolean,
default: false,
},
isDeleted: {
type: Boolean,
default: false,
},
isCommentPassword: {
type: Boolean,
default: false,
},
passwordCommentAlert: {
type: String,
default: '',
},
currentPasswordCommentId: {
type: Number,
},
password: {
type: String,
},
// isEditPushed: {
// type: Boolean,
// required: false,
// },
// isDeletePushed: {
// type: Boolean,
// required: false,
// },
editCommentAlert: String,
});
const isEditPushed = ref(false);
const isDeletePushed = ref(false);
const displayName = computed(() => {
return props.nickname ? props.nickname : props.comment.author;
}); });
// emits // emits
const emit = defineEmits(['submitComment']); const emit = defineEmits([
'submitComment',
'updateReaction',
'editClick',
'deleteClick',
'submitPassword',
'submitEdit',
'cancelEdit',
'update:password',
'inputDetector',
]);
const filterInput = event => {
event.target.value = event.target.value.replace(/\s/g, ''); //
emit('update:password', event.target.value);
};
const localEditedContent = ref(props.comment.content);
const isModifyContent = ref(props.comment.content);
const disabled = ref(false);
// //
const isComment = ref(false); const isComment = ref(false);
@ -59,29 +165,80 @@ const toggleComment = () => {
}; };
// //
const addChildComment = (parentId, newComment) => { const submitComment = newComment => {
emit('submitComment', parentId, newComment); emit('submitComment', { parentId: props.comment.commentId, ...newComment, LOCBRDTYP: newComment.LOCBRDTYP });
isComment.value = false;
}; };
// ,
const handleUpdateReaction = reactionData => {
emit('updateReaction', {
boardId: props.comment.boardId,
commentId: props.comment.commentId || reactionData.commentId,
...reactionData,
});
};
//
const logPasswordAndEmit = () => {
emit('submitPassword', props.comment, props.password);
};
const handleInject = inject('isBtnPushed');
// ,
watch(
() => handleInject.value,
(newValue, oldValue) => {
if (newValue) {
if (newValue.target == props.comment.commentId) {
isEditPushed.value = newValue.isEditPushed;
isDeletePushed.value = newValue.isDeletePushed;
} else {
isEditPushed.value = false;
isDeletePushed.value = false;
}
}
},
);
watch(
() => props.comment.isEditTextarea,
newVal => {
if (newVal) {
localEditedContent.value = props.comment.content;
}
},
);
// text
watch(
() => localEditedContent.value,
newVal => {
if (JSON.stringify(isModifyContent.value) == JSON.stringify(newVal)) {
disabled.value = false;
return;
}
disabled.value = true;
emit('inputDetector');
},
);
// watch(() => props.comment.isDeleted, () => {
// console.log("BoardComment - isDeleted :", newVal);
// if (newVal) {
// localEditedContent.value = " ."; // UI
// props.comment.isEditTextarea = false;
// }
// });
//
const submitEdit = () => {
emit('submitEdit', props.comment, localEditedContent.value);
};
const handleEditClick = () => {
emit('editClick', props.comment);
};
</script> </script>
<style scoped>
/* .twoDepth {
margin-top: 10px;
padding-left: 40px;
}
.list-unstyled > li ~ li {
margin-top: 10px;
}
.btn-text-primary {
padding-left: 0;
}
.btn-text-primary:hover,
.btn-text-primary:active,
.btn-text-primary:focus {
background-color: transparent
} */
</style>

View File

@ -0,0 +1,205 @@
<template>
<div class="card shadow-none border mt-6">
<div class="card-body">
<!-- 댓글 입력 섹션 -->
<div class="d-flex justify-content-start align-items-top">
<div class="w-100">
<textarea
class="form-control mb-1"
placeholder="댓글 달기"
rows="3"
:maxlength="maxLength"
v-model="comment"
@input="clearAlert('comment')"
></textarea>
<span v-if="commentAlert" class="invalid-feedback d-inline text-start ms-2 mb-2">{{ commentAlert }}</span>
<span v-else class="invalid-feedback d-inline">{{ textAlert }}</span>
</div>
</div>
<!-- 옵션 버튼 섹션 -->
<div class="d-flex justify-content-between align-items-center mt-1 pb-4">
<!-- 왼쪽: 익명 체크박스 -->
<div v-if="unknown" class="form-check form-check-inline mb-0 me-2">
<input
class="form-check-input"
type="checkbox"
:id="`checkboxAnnonymous${commnetId}`"
v-model="isCheck"
@change="pwd2AlertHandler"
/>
<label class="form-check-label text-nowrap" :for="`checkboxAnnonymous${commnetId}`">익명</label>
</div>
<!-- 중앙: 닉네임 & 비밀번호 입력 필드 (가로 정렬) -->
<div v-if="isCheck" class="d-flex flex-grow-1 gap-2">
<!-- 닉네임 입력 영역 -->
<div class="position-relative">
<input
type="text"
class="form-control mb-1"
v-model="nickname"
placeholder="닉네임"
@input="clearAlert('nickname')"
@keypress="noSpace"
:maxlength="6"
/>
<!-- 닉네임 경고 메시지 -->
<div v-if="nicknameAlert" class="position-absolute text-danger small top-100 start-0">
{{ nicknameAlert }}
</div>
</div>
<!-- 비밀번호 입력 영역 -->
<div class="position-relative">
<input
type="password"
id="basic-default-password"
class="form-control mb-1"
autocomplete="new-password"
v-model="password"
placeholder="비밀번호"
maxlength="8"
@input="
password = password.replace(/\s/g, '');
clearAlert('password');
"
/>
<!-- 비밀번호 경고 메시지 -->
<div v-if="passwordAlert2" class="position-absolute text-danger small top-100 start-0">
{{ passwordAlert2 }}
</div>
</div>
</div>
<!-- 오른쪽: 답변 쓰기 버튼 -->
<div class="ms-auto">
<SaveBtn class="btn btn-primary" @click="handleCommentSubmit"></SaveBtn>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, defineEmits, defineProps, watch, inject } from 'vue';
import SaveBtn from '../button/SaveBtn.vue';
const props = defineProps({
unknown: {
type: Boolean,
default: true,
},
parentId: {
type: Number,
default: 0,
},
passwordAlert: {
type: String,
default: '',
},
commentAlert: {
type: String,
default: '',
},
maxLength: {
type: Number,
default: 500,
},
commnetId: {
type: Number,
},
});
const noSpace = (e) => {
if (e.key === ' ') e.preventDefault();
};
const $common = inject('common');
const comment = ref('');
const password = ref('');
const isCheck = ref(false);
const textAlert = ref('');
const nicknameAlert = ref('');
const passwordAlert2 = ref('');
const nickname = ref('');
const emit = defineEmits(['submitComment']);
//
const clearAlert = field => {
if (field === 'comment') textAlert.value = '';
if (field === 'nickname') nicknameAlert.value = '';
if (field === 'password') passwordAlert2.value = '';
};
const handleCommentSubmit = () => {
let isValid = true;
//
if (!$common.isNotEmpty(comment.value)) {
textAlert.value = '댓글을 입력해주세요.';
isValid = false;
} else {
textAlert.value = '';
}
// &
if (isCheck.value) {
if (!$common.isNotEmpty(nickname.value)) {
nicknameAlert.value = '닉네임을 입력해주세요.';
isValid = false;
} else {
nicknameAlert.value = '';
}
if (!$common.isNotEmpty(password.value)) {
passwordAlert2.value = '비밀번호를 입력해주세요.';
password.value = '';
isValid = false;
} else {
passwordAlert2.value = '';
}
}
//
if (!isValid) return;
//
emit('submitComment', {
comment: comment.value,
nickname: isCheck.value ? nickname.value : '',
password: isCheck.value ? password.value : '',
isCheck: isCheck.value,
LOCBRDTYP: isCheck.value ? '300102' : null, // '300102'
});
//
resetCommentForm();
};
// &
const pwd2AlertHandler = () => {
if (!isCheck.value) {
passwordAlert2.value = '';
nicknameAlert.value = '';
}
};
//
const resetCommentForm = () => {
comment.value = '';
password.value = '';
nickname.value = '';
isCheck.value = false;
};
watch(
() => props.passwordAlert,
() => {
if (!props.passwordAlert) {
resetCommentForm();
}
},
);
</script>

View File

@ -1,51 +1,176 @@
<template> <template>
<ul class="list-unstyled mt-10"> <ul class="list-unstyled mt-10">
<li <li v-for="comment in comments" :key="comment.commentId" class="mt-6 border-bottom pb-6">
v-for="comment in comments" <BoardComment
:key="comment.id" :unknown="unknown"
class="mt-6" :comment="comment"
:isCommentAuthor="comment.isCommentAuthor"
:isEditTextarea="comment.isEditTextarea"
:isDeleted="isDeleted"
:nickname="comment.nickname"
:isCommentPassword="isCommentPassword"
:passwordCommentAlert="passwordCommentAlert || ''"
:currentPasswordCommentId="currentPasswordCommentId"
:password="password"
:editCommentAlert="editCommentAlert[comment.commentId]"
:is-edit-pushed="comment.isEditPushed"
:is-delete-pushed="comment.isDeletePushed"
@editClick="handleEditClick"
@deleteClick="handleDeleteClick"
@submitPassword="submitPassword"
@submitComment="submitComment"
@submitEdit="(comment, editedContent) => $emit('submitEdit', comment, editedContent)"
@cancelEdit="handleCancelEdit"
@updateReaction="reactionData => handleUpdateReaction(reactionData, comment.commentId, comment.boardId)"
@update:password="updatePassword"
@inputDetector="$emit('inputDetector')"
> >
<BoardComment :comment="comment" @submitComment="addComment" /> <!-- 대댓글 -->
<template #reply>
<ul v-if="comment.children && comment.children.length" class="list-unstyled">
<li v-for="(child, index) in comment.children" :key="child.commentId" class="mt-8 pt-6 ps-10 border-top">
<BoardComment
:comment="child"
:unknown="child.author === '익명'"
:isPlusButton="false"
:isLike="true"
:isCommentProfile="true"
:nickname="child.nickname"
:isCommentAuthor="child.isCommentAuthor"
:isCommentPassword="isCommentPassword"
:currentPasswordCommentId="currentPasswordCommentId"
:passwordCommentAlert="passwordCommentAlert"
:password="password"
:editCommentAlert="editCommentAlert[child.commentId]"
:is-edit-pushed="child.isEditPushed"
:is-delete-pushed="child.isDeletePushed"
@editClick="handleReplyEditClick"
@deleteClick="$emit('deleteClick', child)"
@submitEdit="(comment, editedContent) => $emit('submitEdit', comment, editedContent, child.commentId)"
@cancelEdit="$emit('cancelEdit', child)"
@submitComment="submitComment"
@updateReaction="handleUpdateReaction"
@submitPassword="$emit('submitPassword', child, password)"
@update:password="$emit('update:password', $event)"
@inputDetector="$emit('inputDetector')"
/>
</li>
</ul>
</template>
</BoardComment>
</li> </li>
</ul> </ul>
<Pagination/>
</template> </template>
<script setup> <script setup>
import { ref } from 'vue'; import { defineProps, defineEmits, watch } from 'vue';
import Pagination from '../pagination/Pagination.vue'; import BoardComment from './BoardComment.vue';
import BoardComment from './BoardComment.vue'
const comments = ref([ const props = defineProps({
{ comments: {
id: 1, type: Array,
author: '홍길동', required: true,
content: '저도 궁금합니다.', default: () => [],
children: [
{
id: 2,
author: '사용자1',
content: '저도요!',
}, },
{ unknown: {
id: 3, type: Boolean,
author: '사용자2', default: true,
content: '저도..',
}, },
], isCommentAuthor: {
type: Boolean,
default: false,
}, },
{ isCommentPassword: {
id: 4, type: Boolean,
author: '사용자4', default: false,
content: '흥미로운 주제네요.',
children: [],
}, },
{ isEditTextarea: {
id: 5, type: Boolean,
author: '사용자5', default: false,
content: '우오아아아아아앙',
children: [],
}, },
isDeleted: {
type: Boolean,
default: false,
},
passwordCommentAlert: {
type: String,
default: '',
},
currentPasswordCommentId: {
type: Number,
},
password: {
type: String,
},
index: {
type: Number,
},
editCommentAlert: Object,
});
const emit = defineEmits([
'submitComment',
'updateReaction',
'editClick',
'deleteClick',
'submitPassword',
'clearPassword',
'submitEdit',
'update:password',
'inputDetector',
]); ]);
const submitComment = replyData => {
emit('submitComment', replyData);
};
const handleUpdateReaction = (reactionData, commentId, boardId) => {
const updatedReactionData = {
...reactionData,
commentId: commentId || reactionData.commentId,
boardId: boardId || reactionData.boardId,
};
emit('updateReaction', updatedReactionData);
};
const submitPassword = (comment, password) => {
emit('submitPassword', comment, password);
};
const handleEditClick = comment => {
if (comment.parentId) {
emit('editClick', comment); //
} else {
emit('editClick', comment); //
}
};
const handleSubmitEdit = (comment, editedContent) => {
emit('submitEdit', comment, editedContent);
};
const handleDeleteClick = comment => {
if (comment.parentId) {
emit('deleteClick', comment); //
} else {
emit('deleteClick', comment); //
}
};
const handleCancelEdit = comment => {
if (comment.parentId) {
emit('cancelEdit', comment); //
} else {
emit('cancelEdit', comment); //
}
};
const updatePassword = newPassword => {
emit('update:password', newPassword);
};
const handleReplyEditClick = comment => {
emit('editClick', comment);
};
</script> </script>

View File

@ -3,7 +3,7 @@
<div class="d-flex justify-content-between align-items-center flex-wrap mb-6 gap-2"> <div class="d-flex justify-content-between align-items-center flex-wrap mb-6 gap-2">
<!-- 제목 섹션 --> <!-- 제목 섹션 -->
<div class="me-1"> <div class="me-1">
<h5 class="mb-0">{{ boardTitle }}</h5> <h5 class="mb-0">{{ boardTitle }}adada</h5>
</div> </div>
<!-- 첨부파일 섹션 --> <!-- 첨부파일 섹션 -->
<div v-if="dropdownItems.length > 0" class="btn-group"> <div v-if="dropdownItems.length > 0" class="btn-group">

View File

@ -1,65 +1,83 @@
<template> <template>
<div class="d-flex align-items-center flex-wrap"> <div class="d-flex align-items-center flex-wrap">
<div class="d-flex align-items-center"> <div class="d-flex align-items-center">
<div class="avatar me-2" v-if="unknown"> <div class="avatar me-2 cursor-none">
<img src="/img/avatars/2.png" alt="Avatar" class="rounded-circle" /> <img
:src="getProfileImage(profileImg)"
alt="user"
class="rounded-circle profile-img"
@error="setDefaultImage($event)"
@load="showImage($event)"
/>
</div> </div>
<div class="me-2"> <div class="me-2">
<h6 class="mb-0">{{ profileName }}</h6> <h6 class="mb-0">{{ profileName ? profileName : nickname }}</h6>
<div class="profile-detail"> <div class="profile-detail">
<span>2024.12.10 10:46</span> <span>{{ date }}</span>
<template v-if="showDetail"> <template v-if="showDetail">
<span> <span class="ms-2"> <i class="fa-regular fa-eye"></i> {{ views }} </span>
<i class="fa-regular fa-eye"></i> 1 <span v-if="unknown" class="ms-1"> <i class="bx bx-comment"></i> {{ commentNum }} </span>
</span>
<span>
<i class="fa-regular fa-thumbs-up"></i> 1
</span>
<span>
<i class="fa-regular fa-thumbs-down"></i> 1
</span>
</template> </template>
</div> </div>
</div> </div>
</div> </div>
<div class="ms-auto btn-area"> <!-- 버튼 영역 -->
<template v-if="showDetail"> <div class="ms-auto text-end">
<EditButton @click="handleEdit" /> <!-- 수정, 삭제 버튼 -->
<DeleteButton @click="handleDelete" /> <template v-if="!isDeletedComment && (unknown || isCommentAuthor || isAuthor)">
<div class="float-end ms-1">
<slot name="gobackBtn"></slot>
<EditButton @click.stop="editClick" :is-pushed="isEditPushed" />
<DeleteButton :class="'ms-1'" @click.stop="deleteClick" :is-pushed="isDeletePushed" />
</div>
</template> </template>
<template v-else> <template v-else>
<template v-if="author"> <div class="float-end ms-1">
<EditButton /> <slot name="gobackBtn"></slot>
<DeleteButton /> </div>
<!-- <button class="btn author btn-label-primary btn-icon" @click="handleEdit">
<i class='bx bx-edit-alt'></i>
</button>
<button class="btn author btn-label-primary btn-icon" @click="handleDelete">
<i class='bx bx-trash'></i>
</button> -->
</template>
<BoardRecommendBtn :likeClicked="true" :dislikeClicked="false" :isRecommend="false" />
</template> </template>
<!-- 좋아요, 싫어요 버튼 (댓글에서만 표시) -->
<BoardRecommendBtn
v-if="isLike && !isDeletedComment"
:boardId="boardId"
:comment="comment"
:likeClicked="comment.likeClicked"
:dislikeClicked="comment.dislikeClicked"
@updateReaction="handleUpdateReaction"
/>
</div> </div>
</div> </div>
</template> </template>
<script setup> <script setup>
import { useRouter } from 'vue-router'; import { computed, defineProps, defineEmits, inject, onMounted } from 'vue';
import axios from '@api';
import DeleteButton from '../button/DeleteBtn.vue'; import DeleteButton from '../button/DeleteBtn.vue';
import EditButton from '../button/EditBtn.vue'; import EditButton from '../button/EditBtn.vue';
import BoardRecommendBtn from '../button/BoardRecommendBtn.vue'; import BoardRecommendBtn from '../button/BoardRecommendBtn.vue';
import { onMounted } from 'vue';
// Vue Router
const router = useRouter();
// Props // Props
defineProps({ const props = defineProps({
comment: {
type: Object,
required: false,
},
boardId: {
type: Number,
required: false,
},
commentId: {
type: Number,
required: false,
},
profileName: { profileName: {
type: String, type: String,
default: '익명', default: '',
},
nickname: {
type: String,
default: '',
}, },
unknown: { unknown: {
type: Boolean, type: Boolean,
@ -69,55 +87,78 @@ defineProps({
type: Boolean, type: Boolean,
default: true, default: true,
}, },
author: { isAuthor: {
type: Boolean, type: Boolean,
default: false, default: false,
}, },
isCommentAuthor: Boolean,
isCommentProfile: Boolean,
date: {
type: String,
required: '',
},
views: {
type: Number,
default: 0,
},
commentNum: {
type: Number,
default: 0,
},
isLike: {
type: Boolean,
default: false,
},
profileImg: {
type: String,
default: false,
},
isEditPushed: {
type: Boolean,
require: false,
},
isDeletePushed: {
type: Boolean,
require: false,
},
}); });
const boardId = 100; //!! const emit = defineEmits(['updateReaction', 'editClick', 'deleteClick']);
// const $common = inject('common');
const handleEdit = () => {
router.push({ name: 'BoardEdit', params: { id: boardId } }); const isDeletedComment = computed(() => {
return props.comment?.content === '삭제된 댓글입니다' && props.comment?.updateAtRaw !== props.comment?.createdAtRaw;
});
//
const editClick = () => {
emit('editClick', { ...props.comment, unknown: props.unknown });
}; };
// //
const handleDelete = async () => { const deleteClick = () => {
if (confirm('정말 이 게시물을 삭제하시겠습니까?')) { emit('deleteClick', { ...props.comment, unknown: props.unknown });
try {
await axios.delete(`board/${boardId}`);
alert('게시물이 성공적으로 삭제되었습니다.');
router.push({ name: 'BoardList' });
} catch (error) {
console.error('게시물 삭제 중 오류 발생:', error);
alert('게시물 삭제에 실패했습니다.');
}
}
}; };
// /
const handleUpdateReaction = reactionData => {
emit('updateReaction', {
boardId: props.boardId,
commentId: props.comment?.commentId,
...reactionData,
});
};
//
const getProfileImage = profileImg => {
return $common.getProfileImage(profileImg, props.unknown);
};
const setDefaultImage = e => {
return $common.setDefaultImage(e);
};
const showImage = e => {
return $common.showImage(e);
};
</script> </script>
<style scoped>
.profile-detail span ~ span {
margin-left: 5px;
}
.ms-auto button + button {
margin-left: 5px;
}
.btn.author {
height: 30px;
}
@media screen and (max-width: 450px) {
.btn-area {
margin-top: 10px;
width: 100%;
}
.btn.author {
height: 30px;
}
}
</style>

View File

@ -0,0 +1,12 @@
<template>
<button type="button" class="btn btn-info" @click="$emit('click')">
<i class="bx bx-left-arrow-alt"></i>
</button>
</template>
<script>
export default {
name: "BackButton",
emits: ["click"],
};
</script>

View File

@ -1,14 +1,28 @@
<template v-if="isRecommend"> <template v-if="isRecommend">
<button class="btn btn-label-primary btn-icon" :class="likeClicked ? 'clicked' : '', bigBtn ? 'big' : '' "> <button
<i class="fa-regular fa-thumbs-up"></i> <span class="num">1</span> class="btn btn-label-primary btn-icon me-1"
:class="{ clicked: likeClicked, big: bigBtn, active: props.likeClicked }"
@click="handleLike"
>
<i class="fa-regular fa-thumbs-up"></i> <span class="ms-1">{{ likeCount }}</span>
</button> </button>
<button class="btn btn-label-danger btn-icon" :class="dislikeClicked ? 'clicked' : '', bigBtn ? 'big' : '' "> <button
<i class="fa-regular fa-thumbs-down"></i> <span class="num">1</span> class="btn btn-label-danger btn-icon"
:class="{ clicked: dislikeClicked, big: bigBtn, active: props.dislikeClicked }"
@click="handleDislike"
>
<i class="fa-regular fa-thumbs-down"></i> <span class="ms-1">{{ dislikeCount }}</span>
</button> </button>
</template> </template>
<script setup> <script setup>
defineProps({ import { ref, computed, watch } from 'vue';
const props = defineProps({
comment: {
type: Object,
default: () => ({}),
},
likeClicked: { likeClicked: {
type: Boolean, type: Boolean,
default: false, default: false,
@ -24,53 +38,52 @@ defineProps({
isRecommend: { isRecommend: {
type: Boolean, type: Boolean,
default: true, default: true,
} },
boardId: {
type: Number,
required: true,
},
commentId: {
type: [Number, null],
default: null,
},
likeCount: {
type: Number,
default: 0,
},
dislikeCount: {
type: Number,
default: 0,
},
}); });
const emit = defineEmits(['updateReaction']);
const likeClicked = ref(props.likeClicked);
const dislikeClicked = ref(props.dislikeClicked);
const likeCount = computed(() => props.comment?.likeCount ?? props.likeCount);
const dislikeCount = computed(() => props.comment?.dislikeCount ?? props.dislikeCount);
watch([() => props.likeClicked, () => props.dislikeClicked], ([likeNewval, dislikeNewval]) => {
likeClicked.value = likeNewval;
dislikeClicked.value = dislikeNewval;
});
const handleLike = () => {
const isLike = !likeClicked.value;
const isDislike = false;
emit('updateReaction', { boardId: props.boardId, commentId: props.commentId, isLike, isDislike });
likeClicked.value = isLike;
dislikeClicked.value = false;
};
const handleDislike = () => {
const isDislike = !dislikeClicked.value;
const isLike = false;
emit('updateReaction', { boardId: props.boardId, commentId: props.commentId, isLike, isDislike });
dislikeClicked.value = isDislike;
likeClicked.value = false;
};
</script> </script>
<style scoped>
.btn + .btn {
margin-left: 5px;
}
.num {
margin-left: 5px;
}
.btn-label-danger.clicked {
background-color: #e6381a;
}
.btn-label-danger.clicked i,
.btn-label-danger.clicked span {
color: #fff;
}
.btn-label-primary.clicked {
background-color: #5f61e6;
}
.btn-label-primary.clicked i,
.btn-label-primary.clicked span {
color : #fff;
}
.btn {
width: 55px;
height: 30px;
}
.btn.big {
width: 70px;
height: 70px;
font-size: 18px;
}
@media screen and (max-width:450px) {
.btn {
width: 50px;
height: 20px;
font-size: 12px;
}
}
</style>

View File

@ -1,13 +1,14 @@
<template> <template>
<button class="btn btn-label-primary btn-icon"> <button class="btn btn-label-primary btn-icon" :class="{ active: props.isPushed }">
<i class='bx bx-trash' ></i> <i class="bx bx-trash"></i>
</button> </button>
</template> </template>
<script> <script setup>
export default { const props = defineProps({
name: 'DeleteButton', isPushed: {
methods: { type: Boolean,
required: false,
}, },
}; });
</script> </script>

View File

@ -1,13 +1,38 @@
<template> <template>
<button class="btn btn-label-primary btn-icon"> <button class="btn btn-label-primary btn-icon" :class="{ active: props.isPushed }" @click="toggleText">
<i class="bx bx-edit-alt"></i> <i :class="buttonClass"></i>
</button> </button>
</template> </template>
<script setup>
<script> import { ref, watch, defineEmits, watchEffect } from 'vue';
export default { const props = defineProps({
name: 'EditButton', isToggleEnabled: {
methods: { type: Boolean,
default: false,
}, },
isActive: {
type: Boolean,
required: false,
},
isPushed: {
type: Boolean,
required: false,
},
});
const emit = defineEmits(['click']);
const buttonClass = ref('bx bx-edit-alt');
watchEffect(() => {
buttonClass.value = props.isActive ? 'bx bx-x' : 'bx bx-edit-alt';
});
const toggleText = event => {
//
if (props.isToggleEnabled) {
buttonClass.value = buttonClass.value === 'bx bx-edit-alt' ? 'bx bx-x' : 'bx bx-edit-alt';
}
emit('click', event); //
}; };
const resetButton = () => {
buttonClass.value = 'bx bx-edit-alt';
};
defineExpose({ resetButton });
</script> </script>

View File

@ -0,0 +1,72 @@
<template>
<div class="row gx-2 mb-10 mt-1">
<div class="col-3">
<div class="ratio ratio-1x1">
<!-- 오전 반차 버튼 -->
<button class="vac-btn vac-btn-warning rounded-circle d-flex align-items-center justify-content-center"
:class="{ active: halfDayType === 'AM' }"
@click="toggleHalfDay('AM')">
<i class="bi bi-sun d-flex"></i>
</button>
</div>
</div>
<div class="col-3">
<div class="ratio ratio-1x1">
<!-- 오후 반차 버튼 -->
<button class="vac-btn vac-btn-info rounded-circle d-flex align-items-center justify-content-center"
:class="{ active: halfDayType === 'PM' }"
@click="toggleHalfDay('PM')">
<i class="bi bi-moon d-flex"></i>
</button>
</div>
</div>
<div class="col-3">
<div class="ratio ratio-1x1">
<!-- 연차 버튼 -->
<button class="vac-btn vac-btn-primary rounded-circle d-flex align-items-center justify-content-center"
:class="{ active: halfDayType === 'FULL' }"
@click="toggleHalfDay('FULL')">
<i class="bi bi-calendar d-flex"></i>
</button>
</div>
</div>
<div class="col-3">
<div class="ratio ratio-1x1">
<!-- 저장 버튼 -->
<button class="vac-btn-success rounded-circle d-flex align-items-center justify-content-center"
@click="addVacationRequests"
:class="{ active: !isDisabled, disabled: isDisabled }">
</button>
</div>
</div>
</div>
</template>
<script setup>
import { defineEmits, ref, defineProps } from "vue";
const props = defineProps({
isDisabled: Boolean
});
const emit = defineEmits(["toggleHalfDay", "addVacationRequests", "resetHalfDay"]);
const halfDayType = ref(null);
const toggleHalfDay = (type) => {
halfDayType.value = halfDayType.value === type ? null : type;
emit("toggleHalfDay", halfDayType.value);
};
//
const resetHalfDay = () => {
halfDayType.value = null;
emit("resetHalfDay");
};
const addVacationRequests = () => {
emit("addVacationRequests");
};
defineExpose({ resetHalfDay });
</script>

View File

@ -1,13 +1,14 @@
<template> <template>
<button class="btn btn-label-primary btn-icon"> <button class="btn btn-label-primary btn-icon">
<slot>
<i class="icon-base bx bx-plus"></i> <i class="icon-base bx bx-plus"></i>
</slot>
</button> </button>
</template> </template>
<script> <script>
export default { export default {
name: 'PlusButton', name: 'PlusButton',
methods: { methods: {},
},
}; };
</script> </script>

View File

@ -0,0 +1,18 @@
<template>
<button type="button" class="btn btn-primary ms-1" @click="$emit('click')" :disabled="!isEnabled">
<i class="bx bx-check"></i>
</button>
</template>
<script>
export default {
name: 'SaveButton',
props: {
isEnabled: {
type: Boolean,
default: true, //
},
},
emits: ['click'],
};
</script>

View File

@ -0,0 +1,87 @@
<template>
<!-- 뒤로가기 -->
<button
v-if="canGoBack"
@click="goBack"
:disabled="!canGoBack"
:class="{ 'shifted': showButton }"
class="back-btn rounded-pill btn-icon btn-secondary position-fixed shadow z-5 border-0">
<i class='bx bx-chevron-left'></i>
</button>
<!-- 위로 -->
<button
@click="scrollToTop"
class="scroll-top-btn rounded-pill btn-icon btn-primary position-fixed shadow z-5 border-0"
:class="{ 'visible': showButton, 'hidden': !showButton }"
>
<i class='bx bx-chevron-up'></i>
</button>
</template>
<script setup>
import { ref, onMounted, onUnmounted, watch } from "vue";
import { useRoute, useRouter } from "vue-router";
const showButton = ref(false);
const canGoBack = ref(false);
const route = useRoute();
const router = useRouter();
const loginPagePath = "/login"; //
//
const handleScroll = () => {
showButton.value = window.scrollY > 200;
};
//
const scrollToTop = () => {
window.scrollTo({ top: 0, behavior: "smooth" });
};
//
const goBack = () => {
if (canGoBack.value) {
router.back();
}
};
//
const updateCanGoBack = () => {
const historyBack = router.options.history.state.back;
const previousPage = document.referrer;
// URL
const getPath = (url) => {
try {
return new URL(url, window.location.origin).pathname; //
} catch {
return ""; // URL
}
};
const previousPath = getPath(previousPage);
// :
canGoBack.value = !!historyBack
&& getPath(historyBack) !== loginPagePath
&& !previousPath.startsWith(loginPagePath);
};
//
onMounted(() => {
window.addEventListener("scroll", handleScroll);
updateCanGoBack();
});
// `canGoBack`
watch(route, () => {
updateCanGoBack();
});
//
onUnmounted(() => {
window.removeEventListener("scroll", handleScroll);
});
</script>

View File

@ -1,13 +1,41 @@
<template> <template>
<button class="btn btn-label-primary btn-icon float-end"> <button class="btn btn-label-primary btn-icon float-end" @click="toggleText">
<i class="bx bx-edit"></i> <i :class="buttonClass"></i>
</button> </button>
</template> </template>
<script> <script setup>
export default { import { ref, defineProps, defineExpose, watch } from 'vue';
name: 'WriteButton',
methods: { const props = defineProps({
isToggleEnabled: {
type: Boolean,
default: false,
}, },
isActive: {
type: Boolean,
required: false,
},
});
const buttonClass = ref("bx bx-edit");
watch(
() => props.isActive,
newVal => {
buttonClass.value = newVal ? 'bx bx-x' : 'bx bx-edit';
},
);
const toggleText = () => {
if (props.isToggleEnabled) {
buttonClass.value = buttonClass.value === "bx bx-edit" ? "bx bx-x" : "bx bx-edit";
}
}; };
const resetButton = () => {
buttonClass.value = "bx bx-edit";
};
defineExpose({ resetButton });
</script> </script>

View File

@ -0,0 +1,18 @@
// src/api/holiday.js
import axios from "@api";
export async function fetchHolidays(year, month) {
try {
const response = await axios.get(`vacation/${year}/${month}`);
const holidayEvents = response.data.map((holiday) => ({
title: holiday.name,
start: holiday.date, // "YYYY-MM-DD" 형식
backgroundColor: "#ff6666",
classNames: ["holiday-event"],
}));
return holidayEvents;
} catch (error) {
console.error("공휴일 정보를 불러오지 못했습니다.", error);
return [];
}
}

View File

@ -1,49 +1,65 @@
<template> <template>
<ul class="cate-list list-unstyled d-flex flex-wrap mb-0"> <ul class="cate-list list-unstyled d-flex flex-wrap mb-0">
<li <li v-if="showAll" class="mt-2 me-2">
v-for="category in lists"
:key="category.CMNCODVAL"
class="mt-2 mx-1"
>
<button <button
type="button" type="button"
class="btn" class="btn"
:class="{ :class="{
'btn-outline-primary': category !== selectedCategory, 'btn-outline-primary': selectedCategory !== 'all',
'btn-primary': category === selectedCategory 'btn-primary': selectedCategory === 'all'
}" }"
@click="selectCategory(category)" @click="selectCategory('all')"
>{{ category.CMNCODNAM }}</button> >
All
</button>
</li>
<li v-for="category in lists" :key="category.value" class="mt-2 me-2">
<button
type="button"
class="btn"
:class="{
'btn-outline-primary': category.value.toString() !== selectedCategory?.toString(),
'btn-primary': category.value.toString() === selectedCategory?.toString()
}"
@click="selectCategory(category.value)"
>
{{ category.label }}
</button>
</li> </li>
</ul> </ul>
</template> </template>
<script setup> <script setup>
import { defineProps, ref } from 'vue'; import { defineProps, defineEmits, ref, watch } from 'vue';
// lists prop
const props = defineProps({ const props = defineProps({
lists: { lists: {
type: Array, type: Array,
required: true, required: true,
}, },
showAll: {
type: Boolean,
required: false,
},
selectedCategory: {
type: [String,Number],
default: null,
required: false,
},
}); });
// //
const selectedCategory = ref(null); const selectedCategory = ref(props.selectedCategory);
const selectCategory = (category) => { const emit = defineEmits(['update:data']);
selectedCategory.value = category; const selectCategory = (cate) => {
selectedCategory.value = selectedCategory.value === cate ? null : cate;
emit('update:data', selectedCategory.value);
}; };
watch(() => props.selectedCategory, (newVal) => {
selectedCategory.value = newVal;
});
</script> </script>
<style scoped>
.cate-list {
margin-left: -0.25rem;
}
@media screen and (max-width:450px) {
}
</style>

View File

@ -0,0 +1,223 @@
<template>
<div class="row g-0">
<div class="col-6 pe-1">
<button
class="btn border-3 w-100 py-0 h-px-50"
:class="workTime ? 'p-0 btn-primary pe-none' : 'btn-outline-primary'"
@click="setWorkTime"
>
<i v-if="!workTime" class="bx bx-run fs-2"></i>
<span v-if="workTime" class="ql-size-12px">{{ workTime }}</span>
</button>
</div>
<div class="col-6 ps-1">
<button
class="btn border-3 w-100 py-0 h-px-50"
:class="!workTime ? 'btn-outline-secondary pe-none disabled' : 'btn-outline-secondary'"
@click="setLeaveTime"
:disabled="!workTime"
>
<i v-if="!leaveTime" class='bx bxs-door-open fs-2'></i>
<span v-if="leaveTime" class="ql-size-12px">{{ leaveTime }}</span>
</button>
</div>
</div>
</template>
<script setup>
import { ref, defineProps, defineEmits, onMounted, watch } from 'vue';
import $api from '@api';
import { useGeolocation } from '@vueuse/core';
const props = defineProps({
userId: {
type: Number,
required: false
},
checkedInProject: {
type: Object,
required: false
},
pendingProjectChange: {
type: Object,
default: null
}
});
const emit = defineEmits(['workTimeUpdated', 'leaveTimeUpdated', 'projectChangeComplete', 'update:pendingProjectChange']);
const workTime = ref(null);
const leaveTime = ref(null)
const userLocation = ref(null);
//
const { coords, isSupported, error } = useGeolocation({
enableHighAccuracy: true,
});
//
const getAddress = async (lat, lng) => {
return new Promise((resolve, reject) => {
if (typeof kakao === 'undefined' || !kakao.maps) {
reject('Kakao Maps API가 로드되지 않았습니다.');
return;
}
const geocoder = new kakao.maps.services.Geocoder();
geocoder.coord2Address(lng, lat, (result, status) => {
if (status === kakao.maps.services.Status.OK) {
if (result && result.length > 0 && result[0].address) {
const address = result[0].address.address_name;
resolve(address);
} else {
//
reject('주소 정보가 없습니다.');
}
} else if (status === kakao.maps.services.Status.ZERO_RESULT) {
// ZERO_RESULT
//
geocoder.coord2RegionCode(lng, lat, (regionResult, regionStatus) => {
if (regionStatus === kakao.maps.services.Status.OK && regionResult.length > 0) {
//
const region = regionResult[0].address_name;
resolve(`[대략적 위치] ${region}`);
} else {
//
resolve(`위도: ${lat}, 경도: ${lng} (주소 정보 없음)`);
}
});
} else {
reject(`주소를 가져올 수 없습니다. 상태: ${status}`);
}
});
});
};
//
const getLocation = async () => {
if (!isSupported.value) {
alert('브라우저가 위치 정보를 지원하지 않습니다.');
return null;
}
if (error.value) {
alert(`위치 정보를 가져오는데 실패했습니다: ${error.value.message}`);
return null;
}
if (!coords.value) {
return null;
}
userLocation.value = {
lat: coords.value.latitude,
lng: coords.value.longitude,
};
try {
const address = await getAddress(coords.value.latitude, coords.value.longitude);
return address;
} catch (error) {
alert(error);
return null;
}
};
//
const todayCommuterInfo = async () => {
if (!props.userId) return;
const res = await $api.get(`commuters/today/${props.userId}`);
if (res.status === 200) {
const commuterInfo = res.data.data[0];
if (commuterInfo) {
workTime.value = commuterInfo.COMMUTCMT;
leaveTime.value = commuterInfo.COMMUTLVE;
//
emit('workTimeUpdated', workTime.value);
emit('leaveTimeUpdated', leaveTime.value);
}
}
};
//
const setWorkTime = async () => {
//
if (workTime.value) return;
//
const address = await getLocation();
if (!address) {
//
if (!confirm('위치 정보를 가져오지 못했습니다. 위치 없이 출근 처리하시겠습니까?')) {
return;
}
}
$api.post('commuters/insert', {
memberSeq: props.userId,
projctSeq: props.checkedInProject.PROJCTSEQ,
commutLve: null,
commutArr: address,
commutOut: null,
}).then(res => {
if (res.status === 200) {
todayCommuterInfo();
emit('workTimeUpdated', true);
}
});
};
//
const setLeaveTime = async () => {
//
const address = await getLocation();
if (!address && !leaveTime.value) {
//
if (!confirm('위치 정보를 가져오지 못했습니다. 위치 없이 퇴근 처리하시겠습니까?')) {
return;
}
}
$api.patch('commuters/updateLve', {
memberSeq: props.userId,
commutLve: leaveTime.value || null,
projctLve: props.pendingProjectChange ? props.pendingProjectChange.projctSeq : props.checkedInProject.PROJCTSEQ,
commutOut: address,
}).then(res => {
if (res.status === 200) {
todayCommuterInfo();
emit('leaveTimeUpdated');
emit('update:pendingProjectChange', null);
}
});
};
// props
watch(() => props.userId, async () => {
if (props.userId) {
await todayCommuterInfo();
}
});
onMounted(async () => {
await todayCommuterInfo();
});
//
defineExpose({
todayCommuterInfo,
workTime,
leaveTime
});
</script>

View File

@ -0,0 +1,520 @@
<template>
<div class="container-xxl flex-grow-1 container-p-y">
<div class="card app-calendar-wrapper">
<div class="row g-0">
<div class="col-3 border-end text-center" id="app-calendar-sidebar">
<div class="card-body">
<img v-if="user" :src="`${baseUrl}upload/img/profile/${user.profile}`" alt="Profile Image" class="w-px-50 h-px-50 rounded-circle object-fit-cover" @error="$event.target.src = '/img/icons/icon.png'"/>
<p class="mt-2 fw-bold">
{{ user.name }}
</p>
<CommuterBtn
:userId="user.id"
:checkedInProject="checkedInProject || {}"
:pendingProjectChange="pendingProjectChange"
@update:pendingProjectChange="pendingProjectChange = $event"
@workTimeUpdated="handleWorkTimeUpdate"
@leaveTimeUpdated="handleLeaveTimeUpdate"
ref="workTimeComponentRef"
/>
<CommuterProjectList
:project="project"
:commuters="commuters"
:baseUrl="baseUrl"
:user="user"
:selectedProject="selectedProject"
:checkedInProject="checkedInProject"
@drop="handleProjectDrop"
/>
</div>
</div>
<div class="col app-calendar-content">
<div class="card shadow-none border-0">
<div class="card-body">
<full-calendar
ref="fullCalendarRef"
:events="calendarEvents"
:options="calendarOptions"
defaultView="dayGridMonth"
class="flatpickr-calendar-only"
>
</full-calendar>
<input ref="calendarDatepicker" type="text" class="d-none" />
</div>
</div>
</div>
</div>
</div>
</div>
<CenterModal :display="isModalOpen" @close="closeModal">
<template #title>
{{ eventDate }}
</template>
<template #body>
<div v-if="selectedDateCommuters.length > 0">
<div v-for="(commuter, index) in selectedDateCommuters" :key="index">
<div class="row my-2 d-flex align-items-center">
<div class="col-4">
<img :src="`${baseUrl}upload/img/profile/${commuter.profile}`"
class="me-2 w-px-50 h-px-50 rounded-circle object-fit-cover"
@error="$event.target.src = '/img/icons/icon.png'">
<span class="fw-bold">{{ commuter.memberName }}</span>
</div>
<div class="col-8">
<div class="d-flex gap-1 align-items-center">
출근 :
<MapPopover
:address="commuter.projectAddress"
:is-visible="visiblePopover.type === 'project' && visiblePopover.index === index"
@update-visible="updatePopover('project', index)"
v-if="commuter.projectAddress"
>
<template #trigger>
<div
class="text-white rounded px-2 cursor-pointer"
:style="`background: ${commuter.projctcolor} !important;`"
>
{{ commuter.PROJCTNAM }}
</div>
</template>
</MapPopover>
<span class="ms-auto">
({{ commuter.COMMUTCMT }})
</span>
</div>
<div v-if="commuter.PROJCTLVE" class="d-flex gap-1 mt-1">
퇴근 :
<MapPopover
:address="commuter.leaveProjectAddress"
:is-visible="visiblePopover.type === 'leave' && visiblePopover.index === index"
@update-visible="updatePopover('leave', index)"
v-if="commuter.leaveProjectAddress"
>
<template #trigger>
<div
class="text-white rounded px-2 cursor-pointer"
:style="`background: ${commuter.leaveProjectColor} !important;`"
>
{{ commuter.leaveProjectName }}
</div>
</template>
</MapPopover>
<span class="ms-auto">
({{ commuter.COMMUTLVE || "00:00:00" }})
</span>
</div>
</div>
</div>
</div>
</div>
</template>
<template #footer>
<BackBtn @click="closeModal" />
</template>
</CenterModal>
</template>
<script setup>
import FullCalendar from '@fullcalendar/vue3';
import dayGridPlugin from '@fullcalendar/daygrid';
import interactionPlugin from '@fullcalendar/interaction';
import CenterModal from '@c/modal/CenterModal.vue';
import { computed, inject, nextTick, onMounted, reactive, ref, watch } from 'vue';
import $api from '@api';
import 'flatpickr/dist/flatpickr.min.css';
import '@/assets/css/app-calendar.css';
import { fetchHolidays } from '@c/calendar/holiday';
import { useUserInfoStore } from '@/stores/useUserInfoStore';
import { useProjectStore } from '@/stores/useProjectStore';
import CommuterBtn from '@c/commuters/CommuterBtn.vue';
import CommuterProjectList from '@c/commuters/CommuterProjectList.vue';
import BackBtn from '@c/button/BackBtn.vue';
import MapPopover from '@c/map/MapPopover.vue';
import { useDatePicker } from '@/stores/useDatePicker';
const datePickerStore = useDatePicker();
const baseUrl = $api.defaults.baseURL.replace(/api\/$/, '');
const user = ref({});
const project = ref({});
const userStore = useUserInfoStore();
const projectStore = useProjectStore();
const dayjs = inject('dayjs');
const fullCalendarRef = ref(null);
const workTimeComponentRef = ref(null);
const calendarEvents = ref([]);
const eventDate = ref('');
const selectedProject = ref(null);
const checkedInProject = ref(null);
const isModalOpen = ref(false);
const visiblePopover = ref({
type: null, // 'project' 'leave'
index: null //
});
const commuters = ref([]);
const monthlyCommuters = ref([]);
const calendarDatepicker = ref(null);
const pendingProjectChange = ref(null);
//
const handleWorkTimeUpdate = () => {
todaysCommuter();
loadCommuters();
};
const handleLeaveTimeUpdate = async () => {
await todaysCommuter(); //
// null
const currentUserCommuter = commuters.value.find(c => c.MEMBERSEQ === user.value.id);
if (currentUserCommuter && !currentUserCommuter.COMMUTLVE) {
await projectStore.getMemberProjects();
if (projectStore.activeMemberProjectList.length > 0) {
const previousProject = projectStore.activeMemberProjectList.find(
p => commuters.value.some(c => c.MEMBERSEQ === user.value.id && c.PROJCTLVE === p.PROJCTSEQ)
) || projectStore.activeMemberProjectList[0]; //
if (previousProject) {
selectedProject.value = previousProject.PROJCTSEQ;
projectStore.setSelectedProject(previousProject);
} else if (projectStore.activeProjectList.length > 0) {
selectedProject.value = projectStore.activeProjectList[0].PROJCTSEQ;
projectStore.setSelectedProject(projectStore.activeProjectList[0]);
} else {
selectedProject.value = null;
projectStore.setSelectedProject(null);
}
} else {
selectedProject.value = null;
projectStore.setSelectedProject(null);
}
}
};
// (ProjectList )
const handleProjectDrop = ({ event, targetProject }) => {
const draggedProjectData = JSON.parse(event.dataTransfer.getData('application/json'));
//
if (draggedProjectData.PROJCTSEQ === targetProject.PROJCTSEQ) {
return;
}
pendingProjectChange.value = {
projctSeq: targetProject.PROJCTSEQ,
memberSeq: user.value.id
};
checkedInProject.value = targetProject;
selectedProject.value = targetProject.PROJCTSEQ;
projectStore.setSelectedProject(targetProject);
// UI
commuters.value = commuters.value.map(commuter =>
commuter.MEMBERSEQ === user.value.id
? { ...commuter, PROJCTNAM: targetProject.PROJCTNAM, PROJCTLVE: targetProject.PROJCTSEQ }
: commuter
);
// CommuterBtn (/ )
if (workTimeComponentRef.value && workTimeComponentRef.value.fetchWorkTime) {
workTimeComponentRef.value.fetchWorkTime();
}
};
//
const todaysCommuter = async () => {
const res = await $api.get(`commuters/todays`);
if (res.status === 200 ) {
commuters.value = res.data.data;
}
};
//
const fetchData = async () => {
// FullCalendar API
const calendarApi = fullCalendarRef.value?.getApi();
if (!calendarApi) return;
// ,
const date = calendarApi.currentData.viewTitle;
const dateArr = date.split(' ');
let currentYear = dateArr[0].trim();
let currentMonth = dateArr[1].trim();
const regex = /\D/g;
//
currentYear = parseInt(currentYear.replace(regex, ''), 10);
currentMonth = parseInt(currentMonth.replace(regex, ''), 10);
try {
//
const holidayEvents = await fetchHolidays(currentYear, String(currentMonth).padStart(2, '0'));
//
const existingEvents = calendarEvents.value.filter(event => !event.classNames?.includes('holiday-event'));
//
calendarEvents.value = [...existingEvents, ...holidayEvents];
//
await loadCommuters();
} catch (error) {
console.error('공휴일 정보 로딩 실패:', error);
}
};
// (, , )
const moveCalendar = async (value = 0) => {
const calendarApi = fullCalendarRef.value?.getApi();
if (value === 1) {
calendarApi.prev(); //
} else if (value === 2) {
calendarApi.next(); //
} else if (value === 3) {
calendarApi.today(); //
}
await fetchData();
};
//
const isSelectableDate = (date) => {
const checkDate = dayjs(date);
const isWeekend = checkDate.day() === 0 || checkDate.day() === 6;
//
const isHoliday = calendarEvents.value.some(event =>
event.classNames?.includes('holiday-event') &&
dayjs(event.start).format('YYYY-MM-DD') === checkDate.format('YYYY-MM-DD')
);
return !isWeekend && !isHoliday;
};
//
let todayElement = null;
const handleDateClick = (info) => {
const clickedDate = dayjs(info.date).format('YYYY-MM-DD');
//
const dateCommuters = monthlyCommuters.value.filter(commuter =>
commuter.COMMUTDAY === clickedDate
);
//
if (dateCommuters.length > 0) {
eventDate.value = clickedDate;
isModalOpen.value = true;
}
if (isSelectableDate(info.date)) {
const isToday = dayjs(info.date).isSame(dayjs(), 'day');
if (isToday) {
//
todayElement = info.dayEl;
todayElement.classList.remove('fc-day-today');
} else if (todayElement) {
//
todayElement.classList.add('fc-day-today');
todayElement = null;
}
}
};
// todayElement
document.addEventListener('click', (event) => {
if (todayElement && !event.target.closest('.fc-daygrid-day')) {
todayElement.classList.add('fc-day-today');
todayElement = null;
}
}, true);
//
const getCellClassNames = (arg) => {
const cellDate = dayjs(arg.date);
const classes = [];
// (, , )
if (!isSelectableDate(cellDate)) {
classes.push('fc-day-sat-sun');
}
return classes;
};
//
const loadCommuters = async () => {
const calendarApi = fullCalendarRef.value?.getApi();
if (!calendarApi) return;
const date = calendarApi.currentData.viewTitle;
const dateArr = date.split(' ');
let currentYear = dateArr[0].trim();
let currentMonth = dateArr[1].trim();
const regex = /\D/g;
currentYear = parseInt(currentYear.replace(regex, ''), 10);
currentMonth = parseInt(currentMonth.replace(regex, ''), 10);
const res = await $api.get('commuters/month', {
params: {
year: currentYear,
month: currentMonth
}
});
if (res.status === 200) {
//
monthlyCommuters.value = res.data.data;
document.querySelectorAll('.fc-daygrid-day-events img.rounded-circle').forEach(img => {
img.remove();
});
monthlyCommuters.value.forEach(commuter => {
const date = commuter.COMMUTDAY;
const dateCell = document.querySelector(`.fc-day[data-date="${date}"]`) ||
document.querySelector(`.fc-daygrid-day[data-date="${date}"]`);
if (dateCell) {
const dayEvents = dateCell.querySelector('.fc-daygrid-day-events');
if (dayEvents) {
dateCell.setAttribute('data-has-commuters', 'true');
dayEvents.classList.add('text-center');
//
const profileImg = document.createElement('img');
profileImg.src = `${baseUrl}upload/img/profile/${commuter.profile}`;
profileImg.className = 'rounded-circle w-px-20 h-px-20 mx-1 mb-1 position-relative z-5 m-auto object-fit-cover';
profileImg.style.border = `2px solid ${commuter.projctcolor}`;
profileImg.onerror = () => { profileImg.src = '/img/icons/icon.png'; };
dayEvents.appendChild(profileImg);
}
}
});
}
};
//
const calendarOptions = reactive({
plugins: [dayGridPlugin, interactionPlugin],
initialView: 'dayGridMonth',
headerToolbar: {
left: 'today',
center: 'title',
right: 'prev,next',
},
locale: 'kr',
events: calendarEvents,
eventOrder: 'sortIdx',
contentHeight:"auto",
// eventContent: calendarCommuter,
//
selectable: true,
selectAllow: (selectInfo) => isSelectableDate(selectInfo.start),
dateClick: handleDateClick,
dayCellClassNames: getCellClassNames,
//
unselectAuto: true,
droppable: false,
eventDisplay: 'block',
//
customButtons: {
prev: {
text: 'PREV',
click: () => moveCalendar(1),
},
today: {
text: 'TODAY',
click: () => moveCalendar(3),
},
next: {
text: 'NEXT',
click: () => moveCalendar(2),
},
},
});
// ( )
watch(() => fullCalendarRef.value?.getApi().currentData.viewTitle, async () => {
await fetchData();
});
// selectedProject
watch(() => projectStore.selectedProject, (newProject) => {
if (newProject) {
selectedProject.value = newProject.PROJCTSEQ;
checkedInProject.value = newProject;
} else {
selectedProject.value = null;
checkedInProject.value = null;
}
});
//
const closeModal = () => {
isModalOpen.value = false;
visiblePopover.value = { type: null, index: null };
};
// MapPopover visible
const updatePopover = (popoverType, index) => {
if (visiblePopover.value.type === popoverType && visiblePopover.value.index === index) {
//
visiblePopover.value = { type: null, index: null };
} else {
//
visiblePopover.value = { type: popoverType, index: index };
}
};
const selectedDateCommuters = computed(() => {
return monthlyCommuters.value.filter(commuter =>
commuter.COMMUTDAY === eventDate.value
);
});
onMounted(async () => {
await fetchData();
await userStore.userInfo();
user.value = userStore.user;
await projectStore.getProjectList('', '', 'true');
project.value = projectStore.activeProjectList;
await todaysCommuter();
//
if (projectStore.activeMemberProjectList.length > 0) {
const initialProject = projectStore.getSelectedProject() || projectStore.activeMemberProjectList[0];
selectedProject.value = initialProject?.PROJCTSEQ || null;
projectStore.setSelectedProject(initialProject);
checkedInProject.value = initialProject;
}
datePickerStore.initDatePicker(
fullCalendarRef,
async (year, month, options) => {
//
await fetchData();
}
);
});
</script>

View File

@ -0,0 +1,111 @@
<template>
<div class="commuter-list mt-3">
<div
v-for="post in sortedProjects"
:key="post.PROJCTSEQ"
class="border border-2 mb-3 card p-2"
:style="`border-color: ${post.projctcolor} !important; color: ${post.projctcolor} !important;`"
@dragover="allowDrop($event)"
@drop="handleDrop($event, post)"
>
<p class="mb-1">
{{ post.PROJCTNAM }}
</p>
<div class="row gx-2">
<div
v-for="commuter in commuters.filter(c =>
(c.PROJCTLVE ? c.PROJCTLVE === post.PROJCTSEQ : c.PROJCTNAM === post.PROJCTNAM)
)"
:key="commuter.MEMBERSEQ"
class="col-4"
>
<div class="ratio ratio-1x1">
<img
:src="`${baseUrl}upload/img/profile/${commuter.profile}`"
alt="User Profile"
class="rounded-circle object-fit-cover"
:class="isCurrentUser(commuter) ? 'cursor-pointer' : ''"
:draggable="isCurrentUser(commuter)"
@dragstart="isCurrentUser(commuter) ? dragStart($event, post) : null"
@error="$event.target.src = '/img/icons/icon.png'"
>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { defineProps, defineEmits, computed } from 'vue';
const props = defineProps({
project: {
type: Object,
required: false
},
commuters: {
type: Array,
required: false
},
baseUrl: {
type: String,
required: false
},
user: {
type: Object,
required: false
},
selectedProject: {
type: Number,
default: null
},
checkedInProject: {
type: Object,
default: null
}
});
const emit = defineEmits(['drop', 'update:selectedProject', 'update:checkedInProject']);
//
const sortedProjects = computed(() => {
const projectList = Array.isArray(props.project) ? props.project :
Object.values(props.project || {});
return projectList
.filter(item => item && typeof item === 'object')
.sort((a, b) => (b.participant_count || 0) - (a.participant_count || 0));
});
//
const isCurrentUser = (commuter) => {
//
const isCurrentUserCheck = props.user && commuter && commuter.MEMBERSEQ === props.user.id;
//
const hasNoCheckRecord = !commuter.COMMUTLVE;
// true
return isCurrentUserCheck && hasNoCheckRecord;
};
//
const dragStart = (event, project) => {
//
event.dataTransfer.setData('application/json', JSON.stringify(project));
event.dataTransfer.effectAllowed = 'copy';
};
//
const allowDrop = (event) => {
event.preventDefault();
};
//
const handleDrop = (event, targetProject) => {
event.preventDefault();
emit('drop', { event, targetProject });
};
</script>

View File

@ -41,12 +41,12 @@
<button class="ql-link">Link</button> <button class="ql-link">Link</button>
<button class="ql-image">Image</button> <button class="ql-image">Image</button>
<button class="ql-video">Video</button>
<button class="ql-blockquote">Blockquote</button> <button class="ql-blockquote">Blockquote</button>
<button class="ql-code-block">Code Block</button> <button class="ql-code-block">Code Block</button>
</div> </div>
<!-- 에디터가 표시될 div --> <!-- 에디터가 표시될 div -->
<div ref="editor"></div> <div id="qEditor" ref="editor"></div>
<!-- Alert 메시지 표시 --> <!-- Alert 메시지 표시 -->
<div class="invalid-feedback" :class="isAlert ? 'd-block' : ''">내용을 확인해주세요.</div> <div class="invalid-feedback" :class="isAlert ? 'd-block' : ''">내용을 확인해주세요.</div>
</div> </div>
@ -55,27 +55,43 @@
<script setup> <script setup>
import Quill from 'quill'; import Quill from 'quill';
import 'quill/dist/quill.snow.css'; import 'quill/dist/quill.snow.css';
import { onMounted, ref, watch, defineEmits, defineProps } from 'vue';
import $api from '@api'; import $api from '@api';
import { onMounted, ref, watch, defineEmits, defineProps } from 'vue';
import { useToastStore } from '@s/toastStore';
const toastStore = useToastStore();
const props = defineProps({ const props = defineProps({
isAlert: { isAlert: {
type: Boolean, type: Boolean,
default: false, default: false,
}, },
initialData: {
type: [String, Object],
default: () => null,
},
}); });
const editor = ref(null);
const font = ref('nanum-gothic'); const editor = ref(null); // DOM
const fontSize = ref('16px'); const font = ref('nanum-gothic'); //
const emit = defineEmits(['update:data']); const fontSize = ref('16px'); //
const emit = defineEmits(['update:data', 'update:uploadedImgList', 'update:deleteImgIndexList']);
const uploadedImgList = ref([]); //
const initImageIndex = ref([]); //
const deleteImgIndexList = ref([]); //
onMounted(() => { onMounted(() => {
//
const Font = Quill.import('formats/font'); const Font = Quill.import('formats/font');
Font.whitelist = ['nanum-gothic', 'd2coding', 'consolas', 'serif', 'monospace']; Font.whitelist = ['nanum-gothic', 'd2coding', 'consolas', 'serif', 'monospace'];
Quill.register(Font, true); Quill.register(Font, true);
//
const Size = Quill.import('attributors/style/size'); const Size = Quill.import('attributors/style/size');
Size.whitelist = ['12px', '14px', '16px', '18px', '24px', '32px', '48px']; Size.whitelist = ['12px', '14px', '16px', '18px', '24px', '32px', '48px'];
Quill.register(Size, true); Quill.register(Size, true);
// Quill
const quillInstance = new Quill(editor.value, { const quillInstance = new Quill(editor.value, {
theme: 'snow', theme: 'snow',
placeholder: '내용을 입력해주세요...', placeholder: '내용을 입력해주세요...',
@ -86,74 +102,173 @@ onMounted(() => {
syntax: true, syntax: true,
}, },
}); });
//
quillInstance.format('font', font.value); quillInstance.format('font', font.value);
quillInstance.format('size', fontSize.value); quillInstance.format('size', fontSize.value);
//
quillInstance.on('text-change', () => { quillInstance.on('text-change', () => {
const delta = quillInstance.getContents(); // Get Delta format const delta = quillInstance.getContents(); // Delta
emit('update:data', delta); emit('update:data', delta);
}); });
//
watch([font, fontSize], () => { watch([font, fontSize], () => {
quillInstance.format('font', font.value); quillInstance.format('font', font.value);
quillInstance.format('size', fontSize.value); quillInstance.format('size', fontSize.value);
}); });
// Handle image upload //
let imageUrls = new Set(); watch(uploadedImgList, () => {
quillInstance.getModule('toolbar').addHandler('image', () => { emit('update:uploadedImgList', uploadedImgList.value);
selectLocalImage();
}); });
// ()
watch(deleteImgIndexList, () => {
emit('update:deleteImgIndexList', deleteImgIndexList.value);
});
// , HTML
if (props.initialData) {
quillInstance.setContents(JSON.parse(props.initialData));
initCheckImageIndex();
}
//
quillInstance.getModule('toolbar').addHandler('video', () => {
const url = prompt('YouTube 영상 URL을 입력하세요:');
let src = '';
if (!url || url.trim() == '') return;
// youtube url
if (url.indexOf('watch?v=') !== -1) {
src = url.replace('watch?v=', 'embed/');
// youtu.be URL (ex : https://youtu.be/CfiojceAaeQ?si=G7eM56sdDjIEw-Tz)
} else if (url.indexOf('youtu.be/') !== -1) {
const videoId = url.split('youtu.be/')[1].split('?')[0];
src = `https://www.youtube.com/embed/${videoId}`;
// iframe
} else if (url.indexOf('<iframe') !== -1) {
// DOMParser embeded url
const parser = new DOMParser();
const doc = parser.parseFromString(url, 'text/html');
const iframeEL = doc.querySelector('iframe');
src = iframeEL.getAttribute('src');
} else {
toastStore.onToast('지원하는 영상 타입 아님', 'e');
return;
}
const index = quillInstance.getSelection().index;
quillInstance.insertEmbed(index, 'video', src);
quillInstance.setSelection(index + 1);
});
//
let imageUrls = new Set(); // URL
quillInstance.getModule('toolbar').addHandler('image', () => {
selectLocalImage(); //
});
//
quillInstance.on('text-change', (delta, oldDelta, source) => { quillInstance.on('text-change', (delta, oldDelta, source) => {
// Emit Delta when content changes
emit('update:data', quillInstance.getContents());
delta.ops.forEach(op => { delta.ops.forEach(op => {
if (op.insert && typeof op.insert === 'object' && op.insert.image) { if (op.insert && typeof op.insert === 'object' && op.insert.image) {
const imageUrl = op.insert.image; const imageUrl = op.insert.image; // URL
imageUrls.add(imageUrl); imageUrls.add(imageUrl); // URL
} else if (op.delete) { } else if (op.delete) {
checkForDeletedImages(); checkForDeletedImages(); //
} }
}); });
checkDeletedImages();
emit('update:data', quillInstance.getContents());
}); });
//
async function selectLocalImage() { async function selectLocalImage() {
const input = document.createElement('input'); const input = document.createElement('input');
input.setAttribute('type', 'file'); input.setAttribute('type', 'file');
input.setAttribute('accept', 'image/*'); input.setAttribute('accept', 'image/*');
input.click(); input.click(); //
input.onchange = () => { input.onchange = () => {
const file = input.files[0]; const file = input.files[0];
if (file) { if (file) {
const formData = new FormData(); const formData = new FormData();
formData.append('file', file); formData.append('file', file);
uploadImageToServer(formData).then(serverImageUrl => { // URL
uploadImageToServer(formData)
.then(data => {
const uploadImgIdx = data?.fileIndex; // DB
const serverImageUrl = data?.fileUrl; // url
// ( )
if (uploadImgIdx) {
uploadedImgList.value = [...uploadedImgList.value, uploadImgIdx];
initImageIndex.value = [...initImageIndex.value, uploadImgIdx];
}
const baseUrl = $api.defaults.baseURL.replace(/api\/$/, ''); const baseUrl = $api.defaults.baseURL.replace(/api\/$/, '');
const fullImageUrl = `${baseUrl}${serverImageUrl.replace(/\\/g, '/')}`; //const fullImageUrl = `${baseUrl}${serverImageUrl.replace(/\\/g, '/')}`;
const fullImageUrl = `${baseUrl}${serverImageUrl.replace(/\\/g, '/')}?imgIndex=${uploadImgIdx}`; // index
const range = quillInstance.getSelection(); const range = quillInstance.getSelection();
quillInstance.insertEmbed(range.index, 'image', fullImageUrl); quillInstance.insertEmbed(range.index, 'image', fullImageUrl); //
imageUrls.add(fullImageUrl); imageUrls.add(fullImageUrl); // URL
}).catch(e => { })
.catch(e => {
toastStore.onToast('잠시후 다시 시도해주세요.', 'e'); toastStore.onToast('잠시후 다시 시도해주세요.', 'e');
}); });
} }
}; };
} }
//
async function uploadImageToServer(formData) { async function uploadImageToServer(formData) {
try { try {
// Make the POST request to upload the image
const response = await $api.post('quilleditor/upload', formData, { isFormData: true }); const response = await $api.post('quilleditor/upload', formData, { isFormData: true });
// Check if the response contains the expected data
if (response.data && response.data.data) {
const imageUrl = response.data.data; const imageUrl = response.data.data;
return imageUrl; return imageUrl; // Return the image URL received from the server
} else {
throw new Error('Image URL not returned from server');
}
} catch (error) { } catch (error) {
toastStore.onToast('잠시후 다시 시도해주세요.', 'e'); // Log detailed error information for debugging purposes
console.error('Image upload failed:', error);
// Handle specific error cases (e.g., network issues, authorization issues)
if (error.response) {
// If the error is from the server (e.g., 4xx or 5xx error)
console.error('Error response:', error.response.data);
toastStore.onToast('서버 오류가 발생했습니다. 잠시 후 다시 시도해주세요.', 'e');
} else if (error.request) {
// If no response is received from the server
console.error('No response received:', error.request);
toastStore.onToast('네트워크 오류가 발생했습니다. 잠시 후 다시 시도해주세요.', 'e');
} else {
// If the error is due to something else (e.g., invalid request setup)
console.error('Error message:', error.message);
toastStore.onToast('파일 업로드 중 문제가 발생했습니다. 다시 시도해주세요.', 'e');
}
// Throw the error so the caller knows something went wrong
throw error; throw error;
} }
} }
//
function checkForDeletedImages() { function checkForDeletedImages() {
const editorImages = document.querySelectorAll('#editor img'); const editorImages = document.querySelectorAll('#qEditor img');
const currentImages = new Set(Array.from(editorImages).map(img => img.src)); const currentImages = new Set(Array.from(editorImages).map(img => img.src)); //
imageUrls.forEach(url => { imageUrls.forEach(url => {
if (!currentImages.has(url)) { if (!currentImages.has(url)) {
@ -161,12 +276,41 @@ onMounted(() => {
} }
}); });
} }
//
function initCheckImageIndex() {
const editorImages = document.querySelectorAll('#qEditor img');
const currentImages = new Set(Array.from(editorImages).map(img => img.src)); //
currentImages.forEach(url => {
const index = getImgIndex(url);
if (index) {
initImageIndex.value.push(Number(index));
}
});
}
// index
function getImgIndex(url) {
const params = new URLSearchParams(url.split('?')[1]);
return params.get('imgIndex');
}
//
function checkDeletedImages() {
const editorImages = document.querySelectorAll('#qEditor img');
const currentImages = new Set(Array.from(editorImages).map(img => img.src));
// init
const tempDeleteImgIndex = [...initImageIndex.value];
currentImages.forEach(url => {
const imgIndex = getImgIndex(url);
if (imgIndex) {
const index = tempDeleteImgIndex.indexOf(imgIndex);
tempDeleteImgIndex.splice(index, 1);
}
});
deleteImgIndexList.value = tempDeleteImgIndex;
}
}); });
</script> </script>
<style>
@import 'quill/dist/quill.snow.css';
.ql-editor {
min-height: 300px;
font-family: 'Nanum Gothic', sans-serif;
}
</style>

View File

@ -1,14 +1,16 @@
<template> <template>
<div class="mb-2"> <div class="mb-2 row">
<div class="d-flex align-items-center"> <div class="d-flex">
<label :for="name" class="col-md-2 col-form-label"> <label :for="name" class="col-md-2 col-form-label">
{{ title }} {{ title }}
<span :class="isEssential ? 'link-danger' : 'd-none'">*</span> <span :class="isEssential ? 'link-danger' : 'd-none'">*</span>
</label> </label>
<button type="button" class="btn btn-sm btn-primary" @click="openAddressSearch">주소찾기</button> <div class="align-content-center col-md-10 text-end ms-auto">
<button type="button" class="btn btn-sm btn-primary" :class="isRow ? '' : 'ms-auto'" @click="openAddressSearch">주소찾기</button>
</div>
</div> </div>
<div class="col-md-12"> <div :class="isRow ? 'col-md-10 ms-auto' : 'col-md-12'">
<div class="d-flex mb-3"> <div class="d-flex mb-3">
<input <input
:id="name" :id="name"
@ -16,6 +18,7 @@
type="text" type="text"
v-model="postcode" v-model="postcode"
placeholder="우편번호" placeholder="우편번호"
disabled="true"
readonly readonly
/> />
@ -24,9 +27,9 @@
type="text" type="text"
v-model="address" v-model="address"
placeholder="기본주소" placeholder="기본주소"
disabled="true"
readonly readonly
/> />
</div> </div>
<div> <div>
@ -48,6 +51,10 @@
<script setup> <script setup>
import { ref, watch } from 'vue'; import { ref, watch } from 'vue';
const postcode = ref('');
const address = ref('');
const detailAddress = ref('');
const props = defineProps({ const props = defineProps({
title: { title: {
type: String, type: String,
@ -69,19 +76,37 @@ const props = defineProps({
default: 30, default: 30,
required: false, required: false,
}, },
isRow: {
type: Boolean,
default: false,
required: false,
},
isAlert: { isAlert: {
type: Boolean, type: Boolean,
default: false, default: false,
required: false, required: false,
}, },
modelValue: {
type: Object,
default: () => ({
postcode: '',
address: '',
detailAddress: ''
}),
required: false
}
}); });
const emits = defineEmits(['update:data', 'update:alert']); // watch
watch(() => props.modelValue, (newValue) => {
if (newValue) {
postcode.value = newValue.postcode || '';
address.value = newValue.address || '';
detailAddress.value = newValue.detailAddress || '';
}
}, { immediate: true });
// const emits = defineEmits(['update:data', 'update:alert', 'update:modelValue']);
const postcode = ref('');
const address = ref('');
const detailAddress = ref('');
// //
const openAddressSearch = () => { const openAddressSearch = () => {
@ -116,6 +141,7 @@ const emitAddressData = () => {
detailAddress: detailAddress.value, detailAddress: detailAddress.value,
}; };
emits('update:data', fullAddress); emits('update:data', fullAddress);
emits('update:modelValue', fullAddress); // modelValue
}; };
// isAlert false // isAlert false

View File

@ -1,11 +1,15 @@
<template> <template>
<div class="mb-4 row"> <div class="mb-4 row">
<label :for="name" class="col-md-2 col-form-label">{{ title }}</label> <label :for="inputId" class="col-md-2 col-form-label">{{ title }}</label>
<div class="col-md-10"> <div class="col-md-10">
<label :for="inputId" class="btn btn-label-primary">파일 선택</label>
<input <input
class="form-control" class="form-control"
type="file" type="file"
:id="name" style="display: none"
:id="inputId"
ref="fileInput"
:key="autoIncrement"
@change="changeHandler" @change="changeHandler"
multiple multiple
/> />
@ -21,7 +25,7 @@ import { ref ,computed} from 'vue';
import { fileMsg } from '@/common/msgEnum'; import { fileMsg } from '@/common/msgEnum';
// Props // Props
const prop = defineProps({ const props = defineProps({
title: { title: {
type: String, type: String,
default: '라벨', default: '라벨',
@ -29,7 +33,7 @@ const prop = defineProps({
}, },
name: { name: {
type: String, type: String,
default: 'nameplz', default: 'fileInput',
required: true, required: true,
}, },
isAlert: { isAlert: {
@ -38,22 +42,28 @@ const prop = defineProps({
required: false, required: false,
}, },
}); });
//:key="autoIncrement" .
const inputId = computed(() => props.name || 'defaultFileInput');
const emits = defineEmits(['update:data', 'update:isValid']); const emits = defineEmits(['update:data', 'update:isValid']);
const autoIncrement = ref(props.autoIncrement);
const MAX_TOTAL_SIZE = 5 * 1024 * 1024; // 5MB const MAX_TOTAL_SIZE = 5 * 1024 * 1024; // 5MB
const MAX_FILE_COUNT = 5; // const MAX_FILE_COUNT = 5; //
const ALLOWED_FILE_TYPES = ['image/jpeg', 'image/png', 'application/pdf']; // const ALLOWED_FILE_TYPES = []; //
const showError = ref(false); const showError = ref(false);
const fileMsgKey = ref(''); // const fileMsgKey = ref(''); //
const changeHandler = (event) => { const changeHandler = event => {
const files = Array.from(event.target.files); const files = Array.from(event.target.files);
const totalSize = files.reduce((sum, file) => sum + file.size, 0); const totalSize = files.reduce((sum, file) => sum + file.size, 0);
const invalidFiles = files.filter(file => !ALLOWED_FILE_TYPES.includes(file.type));
// // ALLOWED_FILE_TYPES
const invalidFiles = ALLOWED_FILE_TYPES.length > 0 ? files.filter(file => !ALLOWED_FILE_TYPES.includes(file.type)) : [];
if (totalSize > MAX_TOTAL_SIZE) { if (totalSize > MAX_TOTAL_SIZE) {
showError.value = true; showError.value = true;
fileMsgKey.value = 'FileMaxSizeMsg'; fileMsgKey.value = 'FileMaxSizeMsg';
@ -77,5 +87,5 @@ const changeHandler = (event) => {
} }
}; };
const errorMessage = computed(() => (fileMsg[fileMsgKey.value] || '')); const errorMessage = computed(() => fileMsg[fileMsgKey.value] || '');
</script> </script>

View File

@ -5,6 +5,7 @@
<span v-if="isEssential" class="text-danger">*</span> <span v-if="isEssential" class="text-danger">*</span>
</label> </label>
<div class="col-md-10"> <div class="col-md-10">
<div class="d-flex align-items-center">
<input <input
:id="name" :id="name"
class="form-control" class="form-control"
@ -12,11 +13,19 @@
v-model="inputValue" v-model="inputValue"
:maxLength="maxlength" :maxLength="maxlength"
:placeholder="title" :placeholder="title"
:disabled="disabled"
:min="min"
autocomplete="off"
@focusout="$emit('focusout', modelValue)"
@input="handleInput"
/> />
<div class="invalid-feedback" :class="isAlert ? 'display-block' : ''"> <div v-if="isBtn" class="ms-2">
{{ title }} 확인해주세요. <slot name="append"></slot>
</div> </div>
</div> </div>
<div class="invalid-feedback" :class="isAlert ? 'd-block' : ''">{{ title }} 확인해주세요.</div>
<div class="invalid-feedback" :class="isCateAlert ? 'd-block' : ''">카테고리 중복입니다.</div>
</div>
</div> </div>
</template> </template>
@ -55,34 +64,57 @@ const props = defineProps({
type: Boolean, type: Boolean,
default: false, default: false,
}, },
isCateAlert: {
type: Boolean,
default: false,
},
isLabel: { isLabel: {
type: Boolean, type: Boolean,
default: true, default: true,
required: false, required: false,
}, },
disabled: {
type: Boolean,
default: false,
},
min: {
type: String,
default: '',
required: false,
},
isBtn: {
type: Boolean,
default: false,
required: false,
}
}); });
// Emits // Emits
const emits = defineEmits(['update:modelValue']); const emits = defineEmits(['update:modelValue', 'focusout', 'update:alert']);
// `inputValue` // `inputValue`
const inputValue = ref(props.modelValue); const inputValue = ref(props.modelValue);
// //
watch(inputValue, (newValue) => { watch(inputValue, newValue => {
emits('update:modelValue', newValue); emits('update:modelValue', newValue);
}); });
// //
watch(() => props.modelValue, (newValue) => { watch(
() => props.modelValue,
newValue => {
if (inputValue.value !== newValue) { if (inputValue.value !== newValue) {
inputValue.value = newValue; inputValue.value = newValue;
} }
}); },
</script> );
<style> const handleInput = event => {
.none { const newValue = event.target.value.slice(0, props.maxlength);
display: none;
if (newValue.trim() !== '') {
emits('update:alert', false);
} }
</style> };
</script>

View File

@ -0,0 +1,92 @@
<template>
<div class="input-group">
<input
:id="name"
class="form-control"
:type="type"
v-model="inputValue"
:maxLength="maxlength"
:placeholder="isLabel ? '' : title"
/>
<button class="btn btn-primary" type="button" @click="handleSubmit">확인</button>
</div>
<div class="invalid-feedback" :class="isAlert ? 'display-block' : ''">
{{ title }} 확인해주세요.
</div>
</template>
<script setup>
import { ref, watch } from 'vue';
// Props
const props = defineProps({
title: {
type: String,
default: '라벨',
},
name: {
type: String,
default: 'nameplz',
},
isEssential: {
type: Boolean,
default: false,
},
type: {
type: String,
default: 'text',
},
modelValue: {
type: String,
default: '',
},
maxlength: {
type: Number,
default: 30,
},
isAlert: {
type: Boolean,
default: false,
},
isLabel: {
type: Boolean,
default: true,
},
});
// Emits
const emits = defineEmits(['update:modelValue', 'submit']);
// `inputValue`
const inputValue = ref(props.modelValue);
//
watch(inputValue, (newValue) => {
emits('update:modelValue', newValue);
});
//
watch(() => props.modelValue, (newValue) => {
if (inputValue.value !== newValue) {
inputValue.value = newValue;
}
});
// submit
const handleSubmit = () => {
emits('submit', inputValue.value);
};
</script>
<style scoped>
.invalid-feedback {
display: none;
color: red;
font-size: 0.875rem;
margin-top: 4px;
}
.display-block {
display: block;
}
</style>

View File

@ -2,21 +2,44 @@
<div class="mb-2" :class="isRow ? 'row' : ''"> <div class="mb-2" :class="isRow ? 'row' : ''">
<label :for="name" class="col-md-2 col-form-label" :class="isLabel ? 'd-block' : 'd-none'"> <label :for="name" class="col-md-2 col-form-label" :class="isLabel ? 'd-block' : 'd-none'">
{{ title }} {{ title }}
<span :class="isEssential ? 'link-danger' : 'none'">*</span> <span v-if="isEssential" class="link-danger">*</span>
</label> </label>
<div :class="isRow ? 'col-md-10' : 'col-md-12'"> <div :class="isRow ? 'col-md-10' : 'col-md-12'">
<select class="form-select" :id="name" v-model="selectData"> <div class="d-flex gap-2 align-items-center">
<option v-for="(item, i) in data" :key="i" :value="isCommon ? item.value : i"> <select v-if="isColor && (!data || data.length === 0)" class="form-select" disabled>
<option>사용가능한 컬러가 없습니다</option>
</select>
<!-- 데이터가 있는 경우 원래 select 표시 -->
<select v-else class="form-select" :id="name" v-model="selectData" :disabled="disabled" :style="isColor ? { color: selected } : {}" @blur="$emit('blur')">
<option v-for="(item, i) in data" :key="i" :value="isCommon ? item.value : i" :style="isColor ? { color: item.label } : {}">
{{ isCommon ? item.label : item }} {{ isCommon ? item.label : item }}
</option> </option>
</select> </select>
<div v-if="isBtn" class="ms-2">
<slot name="append"></slot>
</div>
<div v-if="isColor && selected"
class="w-px-40 h-px-30"
:style="{backgroundColor: selected}">
</div>
<img v-if="isMbti && selected"
role="img"
class="w-px-30 h-px-40"
:src="`/img/mbti/${selected.toLowerCase()}.png`"
alt="MBTI image"/>
</div>
<div class="invalid-feedback" :class="isAlert ? 'd-block' : ''">{{ title }} 확인해주세요.</div>
</div> </div>
<div v-if="isAlert" class="invalid-feedback">{{ title }} 확인해주세요.</div>
</div> </div>
</template> </template>
<script setup> <script setup>
import { ref, watch, watchEffect } from 'vue'; import { computed, ref, watch } from 'vue';
const props = defineProps({ const props = defineProps({
title: { title: {
@ -40,7 +63,7 @@ const props = defineProps({
required: true, required: true,
}, },
value: { value: {
type: String, type: [String, Number],
default: '0', default: '0',
require: false, require: false,
}, },
@ -59,23 +82,67 @@ const props = defineProps({
default: true, default: true,
required: false, required: false,
}, },
isBtn: {
type: Boolean,
default: false,
required: false,
},
isCommon: { isCommon: {
type: Boolean, type: Boolean,
default: false, default: false,
required: false, required: false,
} },
disabled: {
type: Boolean,
default: false
},
isColor: {
type: Boolean,
default: false,
required: false,
},
isMbti: {
type: Boolean,
default: false,
required: false,
},
}); });
const emit = defineEmits(['update:data']); const emit = defineEmits(['update:data', 'blur']);
const selectData = ref(props.value); const selectData = ref(props.value);
// props.value watch
watch(() => props.value, (newValue) => {
selectData.value = newValue;
}, { immediate: true });
watchEffect(() => { // data
if (props.isCommon && props.data.length > 0) { watch(() => props.data, (newData) => {
selectData.value = props.data[0].value; // if (props.isCommon && newData.length > 0) {
} else { // value prop '0'()
selectData.value = props.value; // if (props.value === '0') {
} selectData.value = newData[0].value;
emit('update:data', selectData.value); emit('update:data', selectData.value);
})
if (props.isColor) {
emit('blur');
}
}
}
}, { immediate: true });
// selectData
watch(selectData, (newValue) => {
emit('update:data', newValue);
});
const selected = computed(() => {
//
const selectedItem = props.data.find(item =>
props.isCommon ? item.value === selectData.value : props.data.indexOf(item) === selectData.value
);
return selectedItem ? selectedItem.label : null;
});
</script> </script>

View File

@ -5,15 +5,16 @@
<span :class="isEssential ? 'link-danger' : 'd-none'">*</span> <span :class="isEssential ? 'link-danger' : 'd-none'">*</span>
</label> </label>
<div class="col-md-12"> <div class="col-md-12">
<div v-if="useInputGroup" class="input-group mb-3"> <div v-if="useInputGroup" class="input-group mb-1">
<input <input
:id="name" :id="name"
class="form-control" class="form-control"
:type="type" :type="type"
@input="updateInput" @input="updateInput"
:value="computedValue" :value="computedValue"
:disabled="disabled"
:maxLength="maxlength" :maxLength="maxlength"
:placeholder="title" :placeholder="placeholder ? placeholder : title"
@blur="$emit('blur')" @blur="$emit('blur')"
/> />
<span class="input-group-text">@ localhost.co.kr</span> <span class="input-group-text">@ localhost.co.kr</span>
@ -23,11 +24,15 @@
:id="name" :id="name"
class="form-control" class="form-control"
:type="type" :type="type"
:max="type === 'date' ? today : null"
@input="updateInput" @input="updateInput"
:value="computedValue" :value="computedValue"
:disabled="disabled"
:maxLength="maxlength" :maxLength="maxlength"
:placeholder="title" :placeholder="placeholder ? placeholder : title"
@blur="$emit('blur')" @blur="$emit('blur')"
@click="handleDateClick"
ref="inputElement"
/> />
</div> </div>
<div class="invalid-feedback" :class="isAlert ? 'd-block' : ''">{{ title }} 확인해주세요.</div> <div class="invalid-feedback" :class="isAlert ? 'd-block' : ''">{{ title }} 확인해주세요.</div>
@ -36,7 +41,7 @@
</template> </template>
<script setup> <script setup>
import { inject, computed } from 'vue'; import { inject, computed, ref } from 'vue';
const props = defineProps({ const props = defineProps({
title: { title: {
@ -64,6 +69,11 @@
default: '', default: '',
required: false, required: false,
}, },
disabled: {
type: Boolean,
default: false,
required: false,
},
maxlength: { maxlength: {
type: Number, type: Number,
default: 30, default: 30,
@ -79,9 +89,14 @@
default: false, default: false,
required: false, required: false,
}, },
placeholder: {
type: String,
default: ''
},
}); });
const emits = defineEmits(['update:data', 'update:alert', 'blur']); const emits = defineEmits(['update:data', 'update:alert', 'blur']);
const inputElement = ref(null);
// dayjs // dayjs
const dayjs = inject('dayjs'); const dayjs = inject('dayjs');
@ -110,4 +125,12 @@
emits('update:alert', false); emits('update:alert', false);
} }
}; };
// date input
const handleDateClick = (event) => {
if (props.type === 'date' && inputElement.value) {
// : UI
inputElement.value.showPicker();
}
};
</script> </script>

View File

@ -0,0 +1,579 @@
<template>
<div class="card mb-3 shadow-sm border" :class="isProjectExpired ? 'end-project' : ''">
<div class="row g-0">
<div class="card-body">
<!-- 제목 -->
<div class="d-flex justify-content-between">
<h5 class="card-title fw-bold">
{{ title }}
</h5>
<div v-if="!isProjectExpired" class="d-flex gap-1">
<EditBtn @click.stop="openEditModal" />
<DeleteBtn v-if="isProjectCreator" @click.stop="handleDelete" class="ms-1"/>
</div>
</div>
<!-- 날짜 -->
<div class="row align-items-center pb-2">
<div class="col-3 col-md-2 d-flex align-items-center">
<i class="bx bx-calendar"></i>
<div class="ms-2">날짜</div>
</div>
<div class="col-9 col-md-10">
{{ strdate }} ~ {{ enddate }}
</div>
</div>
<!-- 참여자 -->
<div class="row align-items-center pb-2">
<div class="col-3 col-md-2 d-flex align-items-center">
<i class="bx bxs-user"></i>
<div class="ms-2">참여자</div>
</div>
<div class="col-9 col-md-10">
<UserList ref="userListRef" :projctSeq="projctSeq" :showOnlyActive="true" class="mb-0" />
</div>
</div>
<!-- 설명 -->
<div class="row align-items-center pb-2">
<div class="col-3 col-md-2 d-flex align-items-center">
<i class="bx bx-detail"></i>
<div class="ms-2">설명</div>
</div>
<div class="col-9 col-md-10">
{{ description || '-' }}
</div>
</div>
<!-- 주소 -->
<div class="row align-items-center pb-2">
<div class="col-3 col-md-2 d-flex align-items-center">
<MapPopover
:address="address"
:is-visible="isMapVisible"
@update-visible="updatePopover"
>
<template #trigger>
<div class="d-flex align-items-center cursor-pointer">
<i class="bx bxs-map"></i>
<div class="ms-2">주소</div>
</div>
</template>
</MapPopover>
</div>
<div class="col-9 col-md-10 d-flex justify-content-between align-items-center">
<div>{{ address }} {{ addressdtail }}</div>
<button type="button" class="btn text-white" :style="`background-color: ${projctColor} !important;`" @click.stop="openModal">
<i class='bx bx-child'></i>
</button>
</div>
</div>
</div>
</div>
</div>
<!-- 로그 모달 -->
<CenterModal :display="isModalOpen" @close="closeModal">
<template #title> 등록·수정자 </template>
<template #body>
<div v-if="logData.length > 0">
<div
v-for="(log, index) in logData"
:key="index"
class="ms-4 mt-2 border p-3"
>
<p class="mb-1">{{ log.logDate }}</p>
<strong>{{ log.logMessage }}</strong>
</div>
</div>
</template>
<template #footer>
<BackBtn @click="closeModal" />
</template>
</CenterModal>
<!-- 수정 모달 -->
<CenterModal :display="isEditModalOpen" @close="closeEditModal">
<template #title> 프로젝트 수정 </template>
<template #body>
<FormInput
title="프로젝트명"
name="name"
:is-essential="true"
:is-alert="nameAlert"
:modelValue="selectedProject.PROJCTNAM"
@update:modelValue="selectedProject.PROJCTNAM = $event"
@update:alert="nameAlert = $event"
/>
<FormSelect
title="컬러"
name="color"
:is-essential="true"
:is-label="true"
:is-common="true"
:is-color="true"
:data="allColors"
:value="selectedProject.PROJCTCOL"
@update:data="selectedProject.PROJCTCOL = $event"
/>
<div class="mb-2 row">
<label class="col-md-2 col-form-label">
참여자
</label>
<div class="col-md-10">
<UserList class="m-0"
ref="editUserListRef"
:projctSeq="projctSeq"
:showOnlyActive="false"
@user-list-update="handleEditUserListUpdate"
/>
</div>
</div>
<!-- 시작일 -->
<div @click="openStartDatePicker">
<FormInput
title="시작일"
type="date"
name="startDay"
:is-essential="true"
:is-alert="startDayAlert"
:modelValue="selectedProject.PROJCTSTR"
@update:modelValue="selectedProject.PROJCTSTR = $event"
ref="startDateInput"
/>
</div>
<!-- 종료일 -->
<div @click="openEndDatePicker">
<FormInput
title="종료일"
type="date"
name="endDay"
:min="selectedProject.PROJCTSTR"
:modelValue="selectedProject.PROJCTEND"
@update:modelValue="selectedProject.PROJCTEND = $event"
ref="endDateInput"
/>
</div>
<FormInput
title="설명"
name="description"
:modelValue="selectedProject.PROJCTDES"
@update:modelValue="selectedProject.PROJCTDES = $event"
/>
<ArrInput
title="주소"
name="address"
:is-essential="true"
:is-row="true"
:modelValue="{
address: selectedProject.PROJCTARR,
detailAddress: selectedProject.PROJCTDTL,
postcode: selectedProject.PROJCTZIP
}"
@update:data="updateAddress"
/>
</template>
<template #footer>
<BackButton @click="closeEditModal" />
<SaveButton @click="handleUpdate" />
</template>
</CenterModal>
</template>
<script setup>
import { defineProps, onMounted, ref, computed, watch } from 'vue';
import UserList from '@c/user/UserList.vue';
import CenterModal from '@c/modal/CenterModal.vue';
import $api from '@api';
import BackBtn from '@c/button/BackBtn.vue';
import BackButton from '@c/button/BackBtn.vue';
import SaveButton from '@c/button/SaveBtn.vue';
import EditBtn from '../button/EditBtn.vue';
import DeleteBtn from '../button/DeleteBtn.vue';
import FormInput from '@c/input/FormInput.vue';
import FormSelect from '@c/input/FormSelect.vue';
import ArrInput from '@c/input/ArrInput.vue';
import MapPopover from '@c/map/MapPopover.vue';
import { useToastStore } from '@s/toastStore';
import { useUserInfoStore } from '@/stores/useUserInfoStore';
import commonApi, { refreshColorList } from '@/common/commonApi';
import { useProjectStore } from '@/stores/useProjectStore';
//
const toastStore = useToastStore();
const userStore = useUserInfoStore();
const projectStore = useProjectStore();
// Props
const props = defineProps({
title: {
type: String,
required: true,
},
strdate: {
type: String,
required: true,
},
enddate: {
type: String,
required: true,
default: "",
},
description: {
type: String,
required: false,
default: "",
},
address: {
type: String,
required: true,
},
addressdtail: {
type: String,
required: true,
},
addressZip: {
type: String,
required: true,
},
projctSeq: {
type: Number,
required: false
},
projctCol: {
type: Number,
required: false
},
projctColor: {
type: String,
required: false
},
projctCreatorId: {
type: Number,
required: false
},
resetUserSelection: {
type: Boolean,
default: false
},
searchParams: {
type: Object,
default: () => ({ text: '', year: null })
}
});
// Emit
const emit = defineEmits(['update']);
//
const isModalOpen = ref(false);
const logData = ref([]);
const isMapVisible = ref(null);
//
const isEditModalOpen = ref(false);
const originalColor = ref('');
const nameAlert = ref(false);
const startDayAlert = ref(false);
const user = ref(null);
const editUserListRef = ref(null);
const userListRef = ref(null);
const selectedUsers = ref({
activeUsers: [],
disabledUsers: []
});
const startDateInput = ref(null);
const endDateInput = ref(null);
// DOM
let startInputElement = null;
let endInputElement = null;
const openStartDatePicker = () => {
if (startInputElement) {
startInputElement.showPicker();
}
};
const openEndDatePicker = () => {
if (endInputElement) {
endInputElement.showPicker();
}
};
const updatePopover = (visible) => {
isMapVisible.value = visible;
};
//
const handleEditUserListUpdate = (userLists) => {
selectedUsers.value = userLists;
};
const isProjectCreator = computed(() => {
return user.value?.id === props.projctCreatorId;
});
// ( )
const isProjectExpired = computed(() => {
if (!props.enddate) return false;
const today = new Date();
today.setHours(0, 0, 0, 0); //
const endDate = new Date(props.enddate);
endDate.setHours(0, 0, 0, 0); //
return endDate < today;
});
//
const selectedProject = ref({
PROJCTSEQ: props.projctSeq,
PROJCTNAM: props.title,
PROJCTSTR: props.strdate,
PROJCTEND: props.enddate,
PROJCTZIP: props.addressZip,
PROJCTARR: props.address,
PROJCTDTL: props.addressdtail,
PROJCTDES: props.description,
PROJCTCOL: props.projctCol,
projctcolor: props.projctColor,
});
//
const { colorList } = commonApi({
loadColor: true,
colorType: 'YNP',
});
// +
const allColors = computed(() => {
// ( )
const existingColor = {
value: props.projctCol, //
label: props.projctColor //
};
//
const otherColors = colorList.value.filter(color => color.value !== existingColor.value);
//
return [existingColor, ...otherColors];
});
// ::
const updateAddress = addressData => {
selectedProject.value = {
...selectedProject.value,
PROJCTZIP: addressData.postcode,
PROJCTARR: addressData.address,
PROJCTDTL: addressData.detailAddress,
};
};
//
const getLogData = async () => {
const res = await $api.get(`project/log/${props.projctSeq}`);
logData.value = res.data.data;
};
//
const openModal = async () => {
await getLogData();
isModalOpen.value = true;
};
//
const closeModal = () => {
isModalOpen.value = false;
};
//
const openEditModal = async () => {
selectedProject.value = {
PROJCTSEQ: props.projctSeq,
PROJCTNAM: props.title,
PROJCTSTR: props.strdate,
PROJCTEND: props.enddate,
PROJCTZIP: props.addressZip,
PROJCTARR: props.address,
PROJCTDTL: props.addressdtail,
PROJCTDES: props.description,
PROJCTCOL: props.projctCol,
projctcolor: props.projctColor,
};
isEditModalOpen.value = true;
originalColor.value = props.projctCol;
};
//
const closeEditModal = () => {
selectedProject.value = {
PROJCTSEQ: props.projctSeq,
PROJCTNAM: props.title,
PROJCTSTR: props.strdate,
PROJCTEND: props.enddate,
PROJCTZIP: props.addressZip,
PROJCTARR: props.address,
PROJCTDTL: props.addressdtail,
PROJCTDES: props.description,
PROJCTCOL: props.projctCol,
projctcolor: props.projctColor,
};
isEditModalOpen.value = false;
// UserList resetSelection
if (editUserListRef.value) {
editUserListRef.value.resetSelection();
}
};
// selectedUsers
watch(() => selectedUsers.value.activeUsers, (newVal, oldVal) => {
}, { deep: true });
watch(() => selectedUsers.value.disabledUsers, (newVal, oldVal) => {
}, { deep: true });
//
const hasChanges = computed(() => {
//
const basicChanges = selectedProject.value.PROJCTNAM !== props.title ||
selectedProject.value.PROJCTSTR !== props.strdate ||
selectedProject.value.PROJCTEND !== props.enddate ||
selectedProject.value.PROJCTZIP !== props.addressZip ||
selectedProject.value.PROJCTARR !== props.address ||
selectedProject.value.PROJCTDTL !== props.addressdtail ||
selectedProject.value.PROJCTDES !== props.description ||
selectedProject.value.PROJCTCOL !== props.projctCol;
//
const userChanges = editUserListRef.value?.hasUserChanges() || false;
return basicChanges || userChanges;
});
//
watch(
() => selectedProject.value.PROJCTSTR, // (strdate)
(newStartDate) => {
if (newStartDate && new Date(newStartDate) > new Date(selectedProject.value.PROJCTEND)) {
//
selectedProject.value.PROJCTEND = newStartDate;
}
}
);
// resetUserSelection
watch(() => props.resetUserSelection, () => {
if (editUserListRef.value) {
editUserListRef.value.resetSelection();
}
});
//
const handleUpdate = async () => {
nameAlert.value = selectedProject.value.PROJCTNAM.trim() === '';
startDayAlert.value = selectedProject.value.PROJCTSTR.trim() === '';
if (nameAlert.value || startDayAlert.value) {
return;
}
if (!hasChanges.value) {
toastStore.onToast('변경된 내용이 없습니다.', 'e');
return;
}
const disabledMemberSeqs = selectedUsers.value.disabledUsers.map(user => user.MEMBERSEQ);
const res = await $api.patch('project/update', {
projctSeq: selectedProject.value.PROJCTSEQ,
projctNam: selectedProject.value.PROJCTNAM,
projctCol: selectedProject.value.PROJCTCOL,
projctArr: selectedProject.value.PROJCTARR,
projctDtl: selectedProject.value.PROJCTDTL,
projctZip: selectedProject.value.PROJCTZIP,
projctStr: selectedProject.value.PROJCTSTR,
projctEnd: selectedProject.value.PROJCTEND || null,
projctDes: selectedProject.value.PROJCTDES || null,
projctUmb: user.value?.id,
originalColor: originalColor.value === selectedProject.value.PROJCTCOL ? null : originalColor.value,
disabledMembers: disabledMemberSeqs
});
if (res.status === 200) {
toastStore.onToast('수정이 완료 되었습니다.', 's');
//
await projectStore.getProjectList(props.searchParams.text, props.searchParams.year, 'false');
await projectStore.getMemberProjects();
await refreshColorList('YNP');
await editUserListRef.value.fetchProjectParticipation();
await userListRef.value.fetchProjectParticipation();
closeEditModal();
emit('update', props.searchParams);
}
};
//
const handleDelete = () => {
if (confirm('프로젝트를 삭제하시겠습니까?')) {
$api.patch('project/delete', {
projctSeq: props.projctSeq,
projctCol: props.projctCol,
})
.then(res => {
if (res.status === 200) {
toastStore.onToast('프로젝트가 삭제되었습니다.', 's');
projectStore.getProjectList();
projectStore.getMemberProjects();
}
})
}
};
//
onMounted(async () => {
//
await userStore.userInfo();
user.value = userStore.user;
if (startDateInput.value) {
// FormInput input
startInputElement = startDateInput.value.$el.querySelector('input[type="date"]');
}
if (endDateInput.value) {
endInputElement = endDateInput.value.$el.querySelector('input[type="date"]');
}
});
</script>

View File

@ -0,0 +1,233 @@
<template>
<div class="col-md-6 col-lg-4 col-xl-4 order-0 mb-6">
<div class="card text-center h-100">
<!-- 더보기 버튼 -->
<div class="d-flex">
<router-link
:to="{ name: 'BoardList', query: { type: selectedBoard } }"
class="btn btn-primary mr-1 pe-1 ps-1 ms-auto my-auto h-50"
>
more
</router-link>
</div>
<div class="card-body">
<!-- 모달 본문 -->
<div class="modal-body">
<!-- 버튼 영역 -->
<div class="btn-group mb-5" role="group">
<button
type="button"
class="btn"
:class="selectedBoard === 'notices' ? 'btn-primary' : 'btn-outline-primary'"
@click="changeBoard('notices')"
>
공지
</button>
<button
type="button"
class="btn"
:class="selectedBoard === 'general' ? 'btn-primary' : 'btn-outline-primary'"
@click="changeBoard('general')"
>
자유
</button>
<button
type="button"
class="btn"
:class="selectedBoard === 'anonymous' ? 'btn-primary' : 'btn-outline-primary'"
@click="changeBoard('anonymous')"
>
익명
</button>
</div>
<!-- 게시글 미리보기 테이블 -->
<table class="table">
<thead>
<tr>
<!-- 익명게시판은 '닉네임', 나머지는 '작성자' -->
<th class="text-start">
<div class="ms-4">
{{ selectedBoard === 'anonymous' ? '닉네임' : '작성자' }}
</div>
</th>
<th class="text-start" style="width: 65%;">
<div class="ms-4">
제목
</div>
</th>
</tr>
</thead>
<tbody>
<tr
v-for="post in currentList"
:key="post.id"
style="cursor: pointer;"
@click="goDetail(post.id, selectedBoard)"
>
<td class="text-start nickname-ellipsis small">
<div class="ms-4">
{{ selectedBoard === 'anonymous' ? post.nickname : post.author }}
</div>
</td>
<td class="text-start fs-6">
<div class="ms-4">
{{ truncateTitle(post.title) }}
<span v-if="post.commentCount" class="text-danger ml-1 small">
[{{ post.commentCount }}]
</span>
<i v-if="post.img" class="bi bi-image mx-1 small"></i>
<i
v-if="post.hasAttachment.length > 0"
class="bi bi-paperclip ml-1 small"
></i>
<div class="text-muted small small">
{{ post.date }}
<span class="ms-1">
<i class="fa-regular fa-eye small me-1"></i>{{post.views}}
</span>
</div>
</div>
</td>
</tr>
<tr v-if="currentList.length === 0">
<td colspan="3" class="text-center text-muted">게시물이 없습니다.</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue';
import { useRouter } from 'vue-router';
import axios from '@api';
import dayjs from 'dayjs';
import isToday from 'dayjs/plugin/isToday';
import isYesterday from 'dayjs/plugin/isYesterday';
import 'bootstrap-icons/font/bootstrap-icons.css';
dayjs.extend(isToday);
dayjs.extend(isYesterday);
const router = useRouter();
// true
const isModalOpen = ref(true);
// : 'notices', 'general', 'anonymous'
const selectedBoard = ref('notices');
//
const noticeList = ref([]);
const freeList = ref([]);
const anonymousList = ref([]);
// computed
const currentList = computed(() => {
if (selectedBoard.value === 'notices') return noticeList.value;
if (selectedBoard.value === 'general') return freeList.value;
if (selectedBoard.value === 'anonymous') return anonymousList.value;
return [];
});
// : HH:mm, YYYY-MM-DD
const formatDate = dateString => {
const date = dayjs(dateString);
return date.isToday() ? date.format('HH:mm') : date.format('YYYY-MM-DD');
};
// 14 ...
const truncateTitle = title => {
return title.length > 7 ? title.slice(0, 7) + '...' : title;
};
// ( 5)
const fetchNoticePosts = async () => {
try {
const { data } = await axios.get('board/notices', { params: { size: 8 } });
if (data?.data) {
noticeList.value = data.data.map(post => ({
id: post.id,
title: post.title,
date: formatDate(post.date),
rawDate: post.date,
views: post.cnt || 0,
commentCount: post.commentCount,
img: post.firstImageUrl,
author: post.author || '관리자',
nickname: post.nickname || '관리자',
hasAttachment: post.hasAttachment, //
}));
}
} catch (error) {
}
};
// board/general ( 10 5)
const fetchGeneralPosts = async () => {
try {
const { data } = await axios.get('board/general', { params: { size: 16 } });
if (data?.data && data.data.list) {
const freePosts = [];
const anonymousPosts = [];
data.data.list.forEach(post => {
if (post.nickname) {
//
anonymousPosts.push({
id: post.id,
title: post.title,
date: formatDate(post.date),
img: post.firstImageUrl,
rawDate: post.date,
views: post.cnt || 0,
commentCount: post.commentCount,
nickname: post.nickname,
hasAttachment: post.hasAttachment, //
});
} else {
//
freePosts.push({
id: post.id,
title: post.title,
date: formatDate(post.date),
rawDate: post.date,
views: post.cnt || 0,
img: post.firstImageUrl,
commentCount: post.commentCount,
author: post.author || '익명',
hasAttachment: post.hasAttachment, //
});
}
});
freeList.value = freePosts.slice(0, 8);
anonymousList.value = anonymousPosts.slice(0, 8);
}
} catch (error) {
console.error(error);
}
};
//
const changeBoard = type => {
selectedBoard.value = type;
};
// ( )
const goDetail = (id, boardType) => {
router.push({ name: 'BoardDetail', params: { id }, query: { type: boardType } });
};
//
fetchNoticePosts();
fetchGeneralPosts();
</script>
<style scoped>
.table > :not(caption) > * > * {
padding: 0 !important;
}
</style>

View File

@ -0,0 +1,260 @@
<template>
<div class="event-modal position-fixed bg-white shadow rounded" :style="modalStyle">
<!-- 이벤트 선택 화면 -->
<div v-if="!selectedEventType" class="d-flex flex-wrap gap-2 p-2">
<div v-for="event in eventTypes" :key="event.code" class="event-icon-wrapper position-relative">
<img
:src="`${baseUrl}img/main-category-img/main-${event.code}.png`"
class="event-icon-select"
style="width: 25px; height: 25px; cursor: pointer"
@click="handleEventClick(event)"
/>
<!-- X 표시 수정 -->
<span v-if="isEventExists(event.type)" class="cancel-mark"> × </span>
</div>
</div>
<!-- 입력 화면 -->
<div v-else class="p-2" style="min-width: 200px">
<div class="d-flex justify-content-between align-items-center mb-2">
<small>{{ getEventTitle(selectedEventType) }}</small>
<button class="btn-close btn-close-sm" style="font-size: 8px" @click="resetForm"></button>
</div>
<div class="mb-2">
<input
type="text"
class="form-control form-control-sm py-1"
style="height: 25px; font-size: 12px"
placeholder="장소"
v-model="eventPlace"
maxlength="20"
@input="handleChangeInput"
/>
<span v-if="noInputAlert" class="invalid-feedback d-block" style="padding-left: 5px">{{ noInputAlert }}</span>
</div>
<div class="mb-2">
<div @click="focusPicker">
<input
type="time"
class="form-control form-control-sm py-1"
style="height: 0%; font-size: 12px"
v-model="eventTime"
@input="handleChangeInput2"
/>
</div>
<input
ref="timeInput"
type="time"
class="hidden-time-input"
style="height: 0%; font-size: 12px"
v-model="eventTime"
@input="handleChangeInput2"
/>
<span v-if="noInputAlert2" class="invalid-feedback d-block" style="padding-left: 5px">{{ noInputAlert2 }}</span>
</div>
<div class="text-end">
<button class="btn btn-primary btn-sm py-1" style="font-size: 12px; height: 25px; line-height: 1" @click="handleSubmit">
등록
</button>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue';
const props = defineProps({
position: {
type: Object,
required: true,
default: () => ({ x: 0, y: 0 }),
},
selectedDate: {
type: String,
required: true,
},
baseUrl: {
type: String,
required: true,
},
dateEvents: {
type: Array,
},
});
const emit = defineEmits(['select', 'delete', 'insert']);
//
const selectedEventType = ref(null);
const eventPlace = ref('');
const eventTime = ref('');
const noInputAlert = ref(null);
const noInputAlert2 = ref(null);
const timeInput = ref(null);
const focusPicker = () => {
if (timeInput.value) {
timeInput.value.showPicker(); // ( )
timeInput.value.focus(); //
}
};
const eventTypes = [
{ type: 'birthdayParty', code: '300203', title: '생일파티' },
{ type: 'dinner', code: '300204', title: '회식' },
{ type: 'teaTime', code: '300205', title: '티타임' },
{ type: 'workshop', code: '300206', title: '워크샵' },
];
const getEventTitle = type => {
return eventTypes.find(event => event.code === type)?.title || '';
};
const isEventExists = type => {
return props.dateEvents?.some(event => event.type === type);
};
const handleEventClick = event => {
if (isEventExists(event.type)) {
if (confirm('이벤트를 취소하시겠습니까?')) {
emit('delete', {
date: props.selectedDate,
code: event.code,
title: event.title,
});
}
} else {
selectedEventType.value = event.code;
noInputAlert.value = '';
noInputAlert2.value = '';
}
};
const handleSubmit = () => {
if (isValid()) {
emit('insert', {
date: props.selectedDate,
code: selectedEventType.value,
title: getEventTitle(selectedEventType.value),
place: eventPlace.value.trim(),
time: eventTime.value,
});
}
};
//
const isValid = () => {
let isValid = true;
if (!eventPlace.value.trim()) {
noInputAlert.value = '장소를 입력해 주세요';
isValid = false;
} else {
noInputAlert.value = '';
}
if (!eventTime.value) {
noInputAlert2.value = '시간을 입력해 주세요';
isValid = false;
} else {
noInputAlert2.value = '';
}
return isValid;
};
const handleChangeInput = () => {
noInputAlert.value = null;
};
const handleChangeInput2 = () => {
noInputAlert2.value = null;
};
const resetForm = () => {
selectedEventType.value = null;
eventPlace.value = '';
eventTime.value = '';
};
// computed
const modalStyle = computed(() => {
const modalWidth = 200; //
const modalHeight = 150; //
const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight;
let x = props.position?.x || 0;
let y = props.position?.y || 0;
//
if (x + modalWidth > viewportWidth) {
x = viewportWidth - modalWidth - 10;
}
if (x < 0) {
x = 10;
}
if (y + modalHeight > viewportHeight) {
y = viewportHeight - modalHeight - 10;
}
if (y < 0) {
y = 10;
}
return {
left: `${x}px`,
top: `${y}px`,
zIndex: 1050,
maxWidth: '90vw', // 90%
maxHeight: '90vh', // 90%
};
});
</script>
<style scoped>
.event-icon-wrapper {
position: relative;
display: inline-block;
}
.event-icon-select {
transition: transform 0.2s;
}
.event-icon-select:hover {
transform: scale(1.1);
}
.cancel-mark {
position: absolute;
top: -8px;
right: -8px;
width: 16px;
height: 16px;
background-color: #dc3545;
color: white;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
font-weight: bold;
line-height: 1;
}
.event-modal {
min-width: 120px;
max-width: 300px;
overflow: auto;
}
/* 작은 화면에서의 스타일 */
@media (max-width: 576px) {
.event-modal {
min-width: 100px;
font-size: 0.9em;
}
}
</style>

View File

@ -0,0 +1,30 @@
<template>
<div class="ps-2" style="font-size: 13px">
<span class="d-flex align-items-center g-2 font_767"><i class="bx bxs-map pe-1"></i>{{ place }}</span>
<span class="d-flex align-items-center g-2 font_767"
><i class="bx bxs-time-five pe-1"></i>{{ $common.dateFormatter(time, 'T') }}</span
>
</div>
</template>
<script>
export default {
props: {
place: {
type: String,
required: true,
},
time: {
type: String,
required: true,
},
},
};
</script>
<style>
@media (max-width: 767px) {
.font_767 {
font-size: 2rem;
}
}
</style>

View File

@ -0,0 +1,724 @@
<template>
<div class="card app-calendar-wrapper">
<div class="row g-0">
<div class="col-3 border-end text-center" id="app-calendar-sidebar">
<div class="card-body">
<img
v-if="user"
:src="`${profileImgUrl}profile/${user.profile}`"
alt="Profile Image"
class="w-px-50 h-px-50 rounded-circle profile-img"
@error="$event.target.src = '/img/icons/icon.png'"
/>
<p class="mt-2 fw-bold">
{{ user.name }}
</p>
<CommuterBtn
ref="workTimeComponentRef"
:userId="user.id"
:checkedInProject="checkedInProject || {}"
:pendingProjectChange="pendingProjectChange"
@update:pendingProjectChange="pendingProjectChange = $event"
@leaveTimeUpdated="handleLeaveTimeUpdate"
/>
<MainEventList
:categoryList="categoryList"
:baseUrl="baseUrl"
:birthdayList="birthdayList"
:vacationList="vacationList"
:birthdayPartyList="birthdayPartyList"
:dinnerList="dinnerList"
:teaTimeList="teaTimeList"
:workShopList="workShopList"
@handle-click-vacation="handleClickVacation"
/>
</div>
</div>
<div class="col app-calendar-content">
<div class="card shadow-none border-0">
<div class="card-body">
<full-calendar
ref="fullCalendarRef"
:events="calendarEvents"
:options="calendarOptions"
defaultView="dayGridMonth"
class="flatpickr-calendar-only"
>
</full-calendar>
<input ref="calendarDatepicker" type="text" class="d-none" />
</div>
</div>
</div>
</div>
</div>
<EventModal
v-if="showModal"
:position="modalPosition"
:selected-date="selectedDate"
:base-url="baseUrl"
:date-events="currentDateEvents"
@select="handleEventSelect"
@delete="handleEventDelete"
@insert="handleEventInsert"
@close="handleCloseModal"
/>
</template>
<script setup>
import { inject, onMounted, reactive, ref, watch, nextTick } from 'vue';
import { fetchHolidays } from '@c/calendar/holiday';
import { useUserInfoStore } from '@/stores/useUserInfoStore';
import { useProjectStore } from '@/stores/useProjectStore';
import { useToastStore } from '@s/toastStore';
import { useWeatherStore } from '@/stores/useWeatherStore';
import { useDatePicker } from '@/stores/useDatePicker';
import { storeToRefs } from 'pinia';
import router from '@/router';
import FullCalendar from '@fullcalendar/vue3';
import dayGridPlugin from '@fullcalendar/daygrid';
import interactionPlugin from '@fullcalendar/interaction';
import CommuterBtn from '@c/commuters/CommuterBtn.vue';
import MainEventList from '@c/main/MainEventList.vue';
import EventModal from '@c/main/EventModal.vue';
import $api from '@api';
import 'flatpickr/dist/flatpickr.min.css';
import '@/assets/css/app-calendar.css';
const baseUrl = import.meta.env.VITE_DOMAIN;
const profileImgUrl = import.meta.env.VITE_SERVER_IMG_URL;
const user = ref({});
const userStore = useUserInfoStore();
const projectStore = useProjectStore();
const weatherStore = useWeatherStore();
const datePickerStore = useDatePicker();
const { dailyWeatherList } = storeToRefs(weatherStore);
const dayjs = inject('dayjs');
const fullCalendarRef = ref(null);
const workTimeComponentRef = ref(null);
const calendarEvents = ref([]);
const calendarDatepicker = ref(null);
//const dailyWeatherList = ref([]);
const selectedProject = ref(null);
const checkedInProject = ref(null);
const pendingProjectChange = ref(null);
//
const showModal = ref(false);
const modalPosition = ref({ x: 0, y: 0 });
const selectedDate = ref('');
//
const $common = inject('common');
const toastStore = useToastStore();
//
const pressTimer = ref(null);
const longPressDelay = 500; // 0.5
/************* category ***************/
//
const categoryList = ref([]);
const fetchCategoryList = async () => {
const { data } = await $api.get('main/category');
if (data) categoryList.value = [...data.data.filter(categoryInfo => categoryInfo.CMNCODODR != 0)];
};
/************* init ***************/
const monthBirthdayList = ref([]);
const monthVacationList = ref([]);
const monthBirthdayPartyList = ref([]);
const monthDinnerList = ref([]);
const monthTeaTimeList = ref([]);
const monthWorkShopList = ref([]);
const birthdayList = ref([]);
const vacationList = ref([]);
const birthdayPartyList = ref([]);
const dinnerList = ref([]);
const teaTimeList = ref([]);
const workShopList = ref([]);
const currentDateEvents = ref([]);
// , ,
const fetchEventList = async param => {
const { data } = await $api.get(`main/eventList?${param}`);
const res = data?.data;
//
const holidayEvents = calendarEvents.value.filter(event => event.classNames?.includes('holiday-event'));
calendarEvents.value = [...holidayEvents, ...dailyWeatherList.value];
//
if (res?.memberBirthdayList?.length) {
monthBirthdayList.value = [...res.memberBirthdayList];
res.memberBirthdayList.forEach(member => {
addEvent($common.dateFormatter(member.MEMBERBTH, 'YMD'), 'birthday', `${member.MEMBERNAM}`);
});
}
//
if (res?.memberVacationList?.length) {
monthVacationList.value = [...res.memberVacationList];
res.memberVacationList.forEach(member => {
addEvent($common.dateFormatter(member.LOCVACUDT, 'YMD'), 'vacation', `${member.MEMBERNAM}`);
});
}
//
monthBirthdayPartyList.value = [];
monthDinnerList.value = [];
monthTeaTimeList.value = [];
monthWorkShopList.value = [];
if (res?.eventList?.length) {
res.eventList.forEach(item => {
switch (item.CMNCODVAL) {
case 300203:
monthBirthdayPartyList.value = [...monthBirthdayPartyList.value, item];
addEvent($common.dateFormatter(item.LOCEVTTME, 'YMD'), 'birthdayParty', '생일파티');
break;
case 300204:
monthDinnerList.value = [...monthDinnerList.value, item];
addEvent($common.dateFormatter(item.LOCEVTTME, 'YMD'), 'dinner', '회식');
break;
case 300205:
monthTeaTimeList.value = [...monthTeaTimeList.value, item];
addEvent($common.dateFormatter(item.LOCEVTTME, 'YMD'), 'teaTime', '티타임');
break;
case 300206:
monthWorkShopList.value = [...monthWorkShopList.value, item];
addEvent($common.dateFormatter(item.LOCEVTTME, 'YMD'), 'workshop', '워크샵');
break;
}
});
}
};
//
const addEvent = (date, type, title) => {
//
if (type === 'birthday') {
const calendarApi = fullCalendarRef.value?.getApi();
if (calendarApi) {
const calendarDate = calendarApi.currentData.currentDate;
const { year } = $common.formatDateTime(new Date(calendarDate));
const birthDate = $common.dateFormatter(date, 'MD');
date = `${year}-${birthDate}`;
}
}
//
const existingEvent = calendarEvents.value.find(
event => $common.dateFormatter(event.start, 'MD') === $common.dateFormatter(date, 'MD') && event.type == type,
);
//
if (!existingEvent) {
calendarEvents.value.push({
start: date,
type: type,
title: title,
classNames: [`${type}-event`],
});
}
};
//
const useFilterEventList = (month, day) => {
//
if (monthBirthdayList.value) {
birthdayList.value = $common.filterTargetByDate(monthBirthdayList.value, 'MEMBERBTH', month, day);
}
//
if (monthVacationList.value) {
vacationList.value = $common.filterTargetByDate(monthVacationList.value, 'LOCVACUDT', month, day);
}
//
if (monthBirthdayPartyList.value) {
birthdayPartyList.value = $common.filterTargetByDate(monthBirthdayPartyList.value, 'LOCEVTTME', month, day);
}
//
if (monthDinnerList.value) {
dinnerList.value = $common.filterTargetByDate(monthDinnerList.value, 'LOCEVTTME', month, day);
}
//
if (monthTeaTimeList.value) {
teaTimeList.value = $common.filterTargetByDate(monthTeaTimeList.value, 'LOCEVTTME', month, day);
}
//
if (monthWorkShopList.value) {
workShopList.value = $common.filterTargetByDate(monthWorkShopList.value, 'LOCEVTTME', month, day);
}
};
//
const fetchData = async () => {
// FullCalendar API
const calendarApi = fullCalendarRef.value?.getApi();
if (!calendarApi) return;
const date = calendarApi.currentData.currentDate;
const { year, month } = $common.formatDateTime(new Date(date));
try {
//
const holidayEvents = await fetchHolidays(year, month);
calendarEvents.value = [...holidayEvents]; //
//
const param = new URLSearchParams();
param.append('year', year);
param.append('month', month);
param.append('day', '1'); //
await fetchEventList(param);
} catch (error) {
console.error('공휴일 정보 로딩 실패:', error);
}
};
// (, , )
const moveCalendar = async (value = 0) => {
const calendarApi = fullCalendarRef.value?.getApi();
if (value === 1) {
calendarApi.prev(); //
} else if (value === 2) {
calendarApi.next(); //
} else if (value === 3) {
calendarApi.today(); //
}
await fetchData();
};
//
const isSelectableDate = date => {
const checkDate = dayjs(date);
const isWeekend = checkDate.day() === 0 || checkDate.day() === 6; //
//
const isHoliday = calendarEvents.value.some(
event =>
event.classNames?.includes('holiday-event') && dayjs(event.start).format('YYYY-MM-DD') === checkDate.format('YYYY-MM-DD'),
);
return !isWeekend && !isHoliday;
};
//
const getCellClassNames = arg => {
const cellDate = dayjs(arg.date);
const classes = [];
// (, , )
if (!isSelectableDate(cellDate)) {
classes.push('fc-day-sat-sun');
} else {
//
classes.push('clickable');
}
return classes;
};
//
let todayEL = null;
const handleDateClick = info => {
if (isSelectableDate(info.date)) {
if ($common.isToday(info.date)) {
//
todayEL = info.dayEl;
todayEL.classList.remove('fc-day-today');
} else if (todayEL) {
//
todayEL.classList.add('fc-day-today');
todayEL = null;
}
}
const { month, day } = $common.formatDateTime(new Date(info.dateStr));
useFilterEventList(month, day);
};
//
const colorToday = e => {
if (todayEL != null && !todayEL.classList.contains('fc-day-today')) todayEL.classList.add('fc-day-today');
};
//
const handleMouseDown = (date, jsEvent) => {
if (showModal.value) showModal.value = false;
//
const dateEvents = calendarEvents.value.filter(
event => $common.dateFormatter(event.start, 'YMD') === $common.dateFormatter(date, 'YMD'),
);
pressTimer.value = setTimeout(() => {
modalPosition.value = {
x: jsEvent.clientX,
y: jsEvent.clientY,
};
selectedDate.value = date;
currentDateEvents.value = dateEvents;
showModal.value = true;
pressTimer.value = null;
}, longPressDelay);
};
//
const handleMouseUp = () => {
if (pressTimer.value) {
clearTimeout(pressTimer.value);
pressTimer.value = null;
}
};
// api
const toggleEvent = async (date, code, title) => {
const { data } = await $api.post('main/toggleEvent', {
date: date,
code: code,
title: title,
});
if (data?.code === 200) toastStore.onToast(data.message);
const { year, month, day } = $common.formatDateTime(new Date(date));
const param = new URLSearchParams();
param.append('year', year);
param.append('month', month);
param.append('day', day);
await fetchEventList(param);
useFilterEventList(month, day);
};
// api
const insertEvent = async (date, code, title, place, time) => {
const dateTime = $common.dateFormatter(`${date} ${time}`);
const { data } = await $api.post('main/inserEvent', {
date: dateTime,
code: code,
title: title,
place: place,
});
if (data?.code === 200) toastStore.onToast(data.message);
const { year, month, day } = $common.formatDateTime(new Date(date));
const param = new URLSearchParams();
param.append('year', year);
param.append('month', month);
param.append('day', day);
await fetchEventList(param);
useFilterEventList(month, day);
};
//
const handleEventSelect = data => {
toggleEvent(data.date, data.code, data.title);
showModal.value = false;
};
//
const handleEventInsert = data => {
insertEvent(data.date, data.code, data.title, data.place, data.time);
showModal.value = false;
};
//
const handleEventDelete = data => {
toggleEvent(data.date, data.code, data.title);
showModal.value = false;
};
//
const handleCloseModal = () => {
showModal.value = false;
};
//
const handleEventContent = item => {
if (!item.event) return null;
//
if (item.event.classNames?.includes('holiday-event')) {
return {
html: `<div class="holiday-text" style="color: white;">${item.event.title}</div>`,
};
}
//
const eventType = item.event.extendedProps.type;
if (!eventType) return null;
let iconCode = '';
switch (eventType) {
case 'birthday':
iconCode = '300201';
break;
case 'vacation':
iconCode = '300202';
break;
case 'birthdayParty':
iconCode = '300203';
break;
case 'dinner':
iconCode = '300204';
break;
case 'teaTime':
iconCode = '300205';
break;
case 'workshop':
iconCode = '300206';
break;
default:
return null;
}
return {
html: `<img src="${baseUrl}img/main-category-img/main-${iconCode}.png" class="calendar-event-icon" style="width: 20px; height: 20px; margin: 2px;" />`,
};
};
//
const calendarOptions = reactive({
plugins: [dayGridPlugin, interactionPlugin],
initialView: 'dayGridMonth',
headerToolbar: {
left: 'today',
center: 'title',
right: 'prev,next',
},
locale: 'kr',
events: calendarEvents,
eventOrder: 'sortIdx',
contentHeight: 'auto',
eventContent: handleEventContent,
selectable: true,
selectAllow: selectInfo => isSelectableDate(selectInfo.start),
dateClick: handleDateClick,
dayCellDidMount: arg => {
//
addWeatherInfo(arg);
const dateCell = arg.el;
//
dateCell.addEventListener('mousedown', e => {
if (!isSelectableDate(arg.date)) return; //
const date = $common.dateFormatter(arg.date, 'YMD');
handleMouseDown(date, e);
});
dateCell.addEventListener('mouseup', handleMouseUp);
dateCell.addEventListener('mouseleave', handleMouseUp);
},
dayCellClassNames: getCellClassNames,
unselectAuto: true,
droppable: false,
eventDisplay: 'block',
customButtons: {
prev: {
text: 'PREV',
click: () => moveCalendar(1),
},
today: {
text: 'TODAY',
click: () => moveCalendar(3),
},
next: {
text: 'NEXT',
click: () => moveCalendar(2),
},
},
});
//
const addWeatherInfo = arg => {
const dateStr = $common.dateFormatter(arg.date, 'YMD');
//
const theDayWeatherInfo = dailyWeatherList.value.find(weather => weather.date === dateStr);
const dayTopEl = arg.el.querySelector('.fc-daygrid-day-top');
const isWeatherInfoExist = dayTopEl.getElementsByClassName('weather-icon').length > 0; //
if (theDayWeatherInfo && !isWeatherInfoExist) {
let weatherIconUrl = `https://openweathermap.org/img/wn/${theDayWeatherInfo.icon}.png`;
if (theDayWeatherInfo.icon === '01d' || theDayWeatherInfo.icon === '01n') {
weatherIconUrl = '/img/icons/sunny-custom.png';
}
//
const weatherEl = document.createElement('img');
weatherEl.src = weatherIconUrl;
weatherEl.alt = theDayWeatherInfo.description;
weatherEl.className = 'weather-icon';
weatherEl.style.width = '28px';
weatherEl.style.height = '28px';
//
dayTopEl.classList.add('align-items-center');
dayTopEl.prepend(weatherEl); // reverse
}
};
//
watch(dailyWeatherList, async () => {
await nextTick(); // DOM
document.querySelectorAll('.fc-daygrid-day').forEach(dayCell => {
addWeatherInfo({
el: dayCell,
date: dayCell.dataset.date,
});
});
});
const handleWheelEvent = e => {
handleCloseModal();
};
//
const handleClickVacation = () => {
router.push('/vacation');
};
// ( )
watch(
() => fullCalendarRef.value?.getApi().currentData.viewTitle,
async () => {
await fetchData();
},
);
// selectbox
watch(
() => projectStore.selectedProject,
newProject => {
if (newProject) {
selectedProject.value = newProject.PROJCTSEQ;
checkedInProject.value = newProject;
} else {
selectedProject.value = null;
checkedInProject.value = null;
}
},
);
const handleLeaveTimeUpdate = async event => {
const memberSeq = user.value.id;
if (!memberSeq) return;
//
const { data } = await $api.post('main/getUserLeaveRecord', {
memberSeq: memberSeq,
});
const res = data?.data;
if (res && !res?.COMMUTLVE) {
await projectStore.getMemberProjects();
if (projectStore.activeMemberProjectList.length > 0) {
const previousProject =
projectStore.activeMemberProjectList.find(p => res.MEMBERSEQ === user.value.id && res.PROJCTLVE === p.PROJCTSEQ) ||
projectStore.activeMemberProjectList[0]; //
if (previousProject) {
selectedProject.value = previousProject.PROJCTSEQ;
projectStore.setSelectedProject(previousProject);
} else if (projectStore.activeProjectList.length > 0) {
selectedProject.value = projectStore.activeProjectList[0].PROJCTSEQ;
projectStore.setSelectedProject(projectStore.activeProjectList[0]);
} else {
selectedProject.value = null;
projectStore.setSelectedProject(null);
}
} else {
selectedProject.value = null;
projectStore.setSelectedProject(null);
}
}
};
onMounted(async () => {
await userStore.userInfo();
user.value = userStore.user;
//
const storedProject = projectStore.getSelectedProject();
if (storedProject) {
selectedProject.value = storedProject.PROJCTSEQ;
checkedInProject.value = storedProject;
}
//
const { year, month, day } = $common.getToday();
const param = new URLSearchParams();
param.append('year', year);
param.append('month', month);
param.append('day', day);
//
await fetchCategoryList();
await fetchEventList(param);
useFilterEventList(month, day);
//
window.addEventListener('wheel', handleWheelEvent);
window.addEventListener('click', colorToday);
datePickerStore.initDatePicker(fullCalendarRef, async (year, month, options) => {
//
await fetchData();
});
});
</script>
<style scoped>
::v-deep(.fc-h-event) {
background-color: transparent;
}
::v-deep(.event-modal) {
padding: 8px;
border: 1px solid #ddd;
}
::v-deep(.event-icon-select:hover) {
transform: scale(1.1);
transition: transform 0.2s;
}
/* 이벤트 모달 노출 시 텍스트 선택 방지 */
::v-deep(.fc-daygrid-day) {
user-select: none;
}
::v-deep(.fc-daygrid-day-events) {
display: flex;
flex-wrap: wrap;
justify-content: center;
/* align-content: flex-start;
align-items: center;
text-align: center !important; */
}
/* 공휴일만 가로로 넗게 나오게 */
::v-deep(.fc-daygrid-event-harness:has(.holiday-event)) {
width: 100% !important;
}
</style>

View File

@ -0,0 +1,137 @@
<template>
<div class="">
<template v-for="category in categoryList" :key="category.CMNCODVAL">
<div
v-if="
(category.CMNCODVAL === 300201 && birthdayList?.length) ||
(category.CMNCODVAL === 300202 && vacationList?.length) ||
(category.CMNCODVAL === 300203 && birthdayPartyList?.length) ||
(category.CMNCODVAL === 300204 && dinnerList?.length) ||
(category.CMNCODVAL === 300205 && teaTimeList?.length) ||
(category.CMNCODVAL === 300206 && workShopList?.length)
"
@click="category.CMNCODVAL == 300202 ? $emit('handleClickVacation') : ''"
:class="category.CMNCODVAL == 300202 ? 'pointer' : ''"
class="border border-2 mt-3 card p-2"
>
<div class="row g-2 position-relative">
<div class="col-3 mx-0 px-0">
<div class="ratio ratio-1x1">
<img
:src="`${baseUrl}img/main-category-img/main-${category.CMNCODVAL}.png`"
:alt="`${category.CMNCODNAM}`"
@error="$event.target.src = '/img/icons/icon.png'"
/>
</div>
</div>
<div class="col-9 mx-0 px-0 d-flex align-items-center">
<template v-if="category.CMNCODVAL === 300201">
<MainMemberProfile :members="birthdayList" :baseUrl="baseUrl" />
</template>
<template v-if="category.CMNCODVAL === 300202">
<MainMemberProfile :members="vacationList" :baseUrl="baseUrl" />
</template>
<template v-if="category.CMNCODVAL === 300203">
<MainEventBoard :place="birthdayPartyList[0].LOCEVTPLC" :time="birthdayPartyList[0].LOCEVTTME" />
</template>
<template v-if="category.CMNCODVAL === 300204">
<MainEventBoard :place="dinnerList[0].LOCEVTPLC" :time="dinnerList[0].LOCEVTTME" />
</template>
<template v-if="category.CMNCODVAL === 300205">
<MainEventBoard :place="teaTimeList[0].LOCEVTPLC" :time="teaTimeList[0].LOCEVTTME" />
</template>
<template v-if="category.CMNCODVAL === 300206">
<MainEventBoard :place="workShopList[0].LOCEVTPLC" :time="workShopList[0].LOCEVTTME" />
</template>
</div>
</div>
</div>
</template>
</div>
</template>
<script setup>
import { defineEmits } from 'vue';
import MainMemberProfile from '@c/main/MainMemberProfile.vue';
import MainEventBoard from '@c/main/MainEventBoard.vue';
const props = defineProps({
project: {
type: Object,
required: false,
},
categoryList: {
type: Array,
},
commuters: {
type: Array,
required: false,
},
baseUrl: {
type: String,
required: false,
},
user: {
type: Object,
required: false,
},
selectedProject: {
type: Number,
default: null,
},
checkedInProject: {
type: Object,
default: null,
},
birthdayList: {
type: Array,
},
vacationList: {
type: Array,
},
birthdayPartyList: {
type: Array,
},
dinnerList: {
type: Array,
},
teaTimeList: {
type: Array,
},
workShopList: {
type: Array,
},
});
defineEmits(['handleClickVacation']);
</script>
<style scoped>
.event-board {
width: 100%;
padding: 0.5rem;
}
.event-info {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.info-item {
display: flex;
align-items: center;
gap: 0.5rem;
}
.label {
font-weight: 500;
color: #666;
min-width: 3rem;
}
.content {
color: #333;
word-break: break-all;
}
</style>

View File

@ -0,0 +1,32 @@
<template>
<div class="ms-2" style="flex: 1">
<ul class="row gx-1 mb-0 list-inline">
<li class="col-4 me-0" v-for="(member, index) in members" :key="index">
<div class="ratio ratio-1x1 mb-0">
<img
:src="`${profileImgUrl}profile/${member.MEMBERPRF}`"
:style="`border-color: ${member.usercolor} !important;`"
alt="User Profile"
class="rounded-circle border border-2 profile-img"
@error="$event.target.src = '/img/icons/icon.png'"
/>
</div>
</li>
</ul>
</div>
</template>
<script setup>
const props = defineProps({
members: {
type: Array,
required: true,
},
baseUrl: {
type: String,
required: true,
},
});
const profileImgUrl = import.meta.env.VITE_SERVER_IMG_URL;
</script>

View File

@ -0,0 +1,244 @@
<template>
<div class="col-md-6 col-lg-4 col-xl-4 order-0 mb-6">
<div class="card h-100">
<!-- 더보기 버튼-->
<div class="d-flex">
<router-link
to="/voteboard"
class="btn btn-primary mr-1 pe-1 ps-1 ms-auto my-auto h-50"
>
more
</router-link>
</div>
<div class="card-header d-flex justify-content-between">
<div class="card-title mb-0">
<h5 class="mb-1 me-2">투표진행</h5>
</div>
</div>
<div class="card-body" v-if="voteListData.length > 0">
<ul class="p-0 m-0">
<li class="d-flex mb-1" v-for="item in voteListData" :key="item.LOCVOTSEQ">
<div class="d-flex w-100 flex-wrap align-items-center justify-content-between gap-2">
<div class="me-2 mb-3">
<div class="text-muted small">{{ item.localVote.formatted_LOCVOTRDT }}</div>
<div class="d-flex flex-wrap align-items-center">
<div class="avatar flex-shrink-0 me-1">
<img
style="cursor: auto;"
class="rounded-circle user-avatar object-fit-cover"
:src="getProfileImage(item.localVote.MEMBERPRF)"
alt="최초 작성자"
:style="{ borderColor: item.localVote.usercolor }"
@error="setDefaultImage"
/>
</div>
<div class="timeline-event ps-1" style="cursor: pointer;" @click.stop="openModal(item.localVote.LOCVOTSEQ)" >
<div class="timeline-header ">
<small ><strong>{{ truncateTitle(item.localVote.LOCVOTTTL) }}</strong></small>
</div>
<small class="d-flex align-items-center lh-1 me-4 mb-4 mb-sm-0"
:style="{ color: getDaysAgo(item.localVote.formatted_LOCVOTEDT) == '금일 종료' ? 'red' : '' }">
{{getDaysAgo(item.localVote.formatted_LOCVOTEDT)}}({{item.localVote.total_voted}}/{{ item.localVote.total_votable }})
</small>
</div>
</div>
</div>
</div>
</li>
</ul>
</div>
<div class="card-body" v-else>
진행중인 투표가 없습니다.
</div>
</div>
</div>
<!--투표 모달 -->
<CenterModal :display="isModalOpen" @close="closeModal">
<template #title> 투표 하기 </template>
<template #body>
<div>
<vote-list
:key="voteListKey"
:data="selectVoteDate"
@checkedNames="checkedNames"
@addContents="addContents"
@endVoteId="endVoteId"
@voteDelete="voteDelete"
/>
</div>
</template>
<template #footer>
<BackButton @click="closeModal" />
</template>
</CenterModal>
</template>
<script setup>
import router from '@/router';
import $api from '@api';
import { onMounted, ref } from 'vue';
import CenterModal from '@c/modal/CenterModal.vue';
import BackButton from '@c/button/BackBtn.vue';
import voteList from '@c/voteboard/voteCardList.vue';
import { useToastStore } from '@s/toastStore';
const toastStore = useToastStore();
const currentPage = ref(1);
const voteset = ref(0);
const voteListData= ref([]);
const voteListKey = ref(0); //
//
const isModalOpen = ref(false);
const selectVoteDate = ref([]);
//
const openModal = async (id) => {
isModalOpen.value = true;
if(id){
const selectData = voteListData.value.filter((item) => item.localVote.LOCVOTSEQ === id);
selectVoteDate.value = selectData;
}
};
//
const closeModal = () => {
isModalOpen.value = false;
voteListKey.value++;
};
//
const baseUrl = $api.defaults.baseURL.replace(/api\/$/, '');
const defaultProfile = "/img/icons/icon.png";
const getProfileImage = (profilePath) => {
return profilePath && profilePath.trim() ? `${baseUrl}upload/img/profile/${profilePath}` : defaultProfile;
};
const setDefaultImage = (event) => {
event.target.src = defaultProfile;
};
onMounted(() => {
getvoteList();
});
//
const getvoteList = () => {
$api.get('vote/getVoteList',{
params:
{
page: 1
,voteset:'2' //
,myVote:'2' //
}
}).then(res => {
voteListData.value = res.data.data.list;
voteListData.value = res.data.data.list.slice(0, 6);
})
};
//
const checkedNames = (numList) => {
$api.post('vote/insertCheckedNums',{
checkedList :numList
,votenum : numList[0].LOCVOTSEQ
}).then((res)=>{
if(res.data.status === 'OK'){
toastStore.onToast('투표가 완료되었습니다.', 's');
isModalOpen.value = false;
getvoteList();
voteListKey.value++;
}
})
}
//
const addContents = (itemList, voteId) => {
$api.post('vote/insertWord',{
itemList :itemList
,voteId :voteId
}).then((res)=>{
if(res.data.status === 'OK'){
toastStore.onToast('항목이 등록되었습니다.', 's');
getvoteList();
const updatedVote = selectVoteDate.value.find(vote => vote.localVote.LOCVOTSEQ === voteId);
if (updatedVote) {
if (!updatedVote.voteDetails) {
updatedVote.voteDetails = [];
}
const maxSeq = updatedVote.voteDetails.reduce((max, item) => {
return item.VOTDETSEQ > max ? item.VOTDETSEQ : max;
}, 0);
// voteDetails
itemList.forEach(item => {
updatedVote.voteDetails.push({
VOTDETSEQ: maxSeq + 1,
LOCVOTSEQ: voteId,
LOCVOTCON: item.content,
LOCVOTLIK: item.url,
VOTE_COUNT: 0,
yesvote: 0
});
});
}
}
})
}
//
const endVoteId = (endVoteId) => {
if(confirm('투표를 종료하시겠습니까?')){
$api.patch('vote/updateEndData',{
endVoteId :endVoteId
}).then((res)=>{
if(res.data.status === 'OK'){
getvoteList();
isModalOpen.value = false;
}
})
}
}
//
const voteDelete =(id) =>{
if(confirm('투표를 삭제하시겠습니까?')){
$api.patch('vote/updateDeleteData',{
deleteVoteId :id
}).then((res)=>{
if(res.data.status === 'OK'){
toastStore.onToast('투표가 삭제되었습니다.', 's');
getvoteList();
isModalOpen.value = false;
}
})
}
}
// 14 ...
const truncateTitle = title => {
return title.length > 10 ? title.slice(0, 10) + '...' : title;
};
//
const goVoteList = () =>{
router.push({
path: '/voteboard',
query: {
voteset: '2' //
,myVote:'2' //
,id:id
}
});
}
//
const getDaysAgo = (dateString) => {
const inputDate = new Date(dateString); // Date
const today = new Date(); //
const input = new Date(inputDate.getFullYear(), inputDate.getMonth(), inputDate.getDate());
const now = new Date(today.getFullYear(), today.getMonth(), today.getDate());
const timeDiff = now - input;
const dayDiff = Math.floor(timeDiff / (1000 * 60 * 60 * 24));
//
if (dayDiff === 0) return "금일 종료";
return `종료 ${Math.abs(dayDiff)}일 전`;
};
</script>
<style scoped>
.user-avatar {
border: 3px solid;
}
</style>

View File

@ -0,0 +1,139 @@
<template>
<div class="col-md-6 col-lg-4 col-xl-4 order-0 mb-6">
<div class="card h-100">
<!-- 더보기 버튼 -->
<div class="d-flex ">
<router-link
to="/wordDict"
class="btn btn-primary mr-1 pe-1 ps-1 ms-auto my-auto h-50"
>
more
</router-link>
</div>
<div class="card-header d-flex justify-content-between">
<div class="card-title mb-0">
<h5 class="mb-1 me-2">용어집</h5>
</div>
</div>
<div class="card-body" v-if="wordList.length > 0">
<ul class="p-0 m-0" v-for="item in wordList" :key="item.WRDDICSEQ">
<li class="d-flex align-items-center mb-1">
<!-- 프로필 이미지 -->
<div class="avatar flex-shrink-0 me-2 d-flex align-items-center">
<img
class="rounded-circle user-avatar object-fit-cover"
:src="getProfileImage(item.lastEditor.profileImage)"
alt="최종 작성자"
:style="{ borderColor: item.lastEditor.color }"
@error="setDefaultImage"
/>
</div>
<!-- 텍스트 영역 -->
<div class="timeline-event ps-1 flex-grow-1" style="cursor: pointer;" @click="goWordList(item.WRDDICCAT,item.WRDDICTTL)">
<div class="timeline-header">
<small class="text-primary text-uppercase">{{ item.category }}</small>
</div>
<h6 class="my-50 d-flex align-items-center">
{{ truncateTitle(item.WRDDICTTL) }}
</h6>
<div class="text-muted small">{{$common.dateFormatter(item.lastEditor.updatedAt)}}</div>
</div>
</li>
</ul>
</div>
<div class="card-body" v-else >
등록된 용어가 없습니다.
</div>
</div>
</div>
</template>
<script setup>
import axios from '@api';
import { getCurrentInstance, onMounted, ref } from 'vue';
import { useRouter } from 'vue-router';
import commonApi from '@/common/commonApi';
const { appContext } = getCurrentInstance();
const $common = appContext.config.globalProperties.$common;
const router = useRouter();
onMounted(() => {
getwordList();
});
//
const baseUrl = axios.defaults.baseURL.replace(/api\/$/, '');
const defaultProfile = "/img/icons/icon.png";
const getProfileImage = (profilePath) => {
return profilePath && profilePath.trim() ? `${baseUrl}upload/img/profile/${profilePath}` : defaultProfile;
};
const setDefaultImage = (event) => {
event.target.src = defaultProfile;
};
const wordList = ref([]);
//
const getwordList = (searchKeyword='', indexKeyword='', category='') => {
axios.get('worddict/getWordList',{
params: {
searchKeyword : searchKeyword,
indexKeyword :indexKeyword,
category : category,
pageNum:6
}
})
.then(res => {
wordList.value = res.data.data.data;
});
};
// /
const getFirstCharacter = (char) => {
const CHOSUNG_LIST = [
'ㄱ', 'ㄲ', 'ㄴ', 'ㄷ', 'ㄸ', 'ㄹ', 'ㅁ', 'ㅂ', 'ㅃ', 'ㅅ',
'ㅆ', 'ㅇ', 'ㅈ', 'ㅉ', 'ㅊ', 'ㅋ', 'ㅌ', 'ㅍ', 'ㅎ'
];
if (!char || char.length === 0) return '';
const code = char.charCodeAt(0);
// (~)
if (code >= 0xAC00 && code <= 0xD7A3) {
const index = Math.floor((code - 0xAC00) / (21 * 28));
return CHOSUNG_LIST[index];
}
//
if (char.match(/[a-zA-Z]/)) {
return char.toLowerCase();
}
// (, )
return char;
};
//
const goWordList = (category, indexKeyword) => {
const firstChar = getFirstCharacter(indexKeyword[0]); //
router.push({
path: '/wordDict',
query: {
indexKeyword: firstChar,
category: category,
}
});
};
// 14 ...
const truncateTitle = title => {
return title.length > 25 ? title.slice(0, 25) + '...' : title;
};
</script>
<style scoped>
.user-avatar {
border: 3px solid;
}
</style>

View File

@ -0,0 +1,120 @@
<template>
<div class="row g-4 mt-2" v-if="memberList?.length">
<div>
<div class="card">
<div class="card-body p-3 row">
<div class="d-flex justify-content-between mb-2">
<h5 class="card-title fw-bold">사원 등록 관리</h5>
</div>
<div class="g-4 col-12 col-lg-6" v-for="member in memberList" :key="member.MEMBERSEQ">
<div class="card">
<div class="row card-body">
<div class="col-3 d-flex align-items-center">
<img
:src="`${imgURL}profile/${member.MEMBERPRF}`"
alt="Profile Image"
class="img-thumbnail mx-auto d-block"
style="max-height: 140px"
@error="$event.target.src = '/img/icons/icon.png'"
/>
</div>
<div class="col-7">
<div class="d-flex flex-sm-row align-items-center pb-2">
<div class="font-bold">{{ member.MEMBERNAM }}</div>
<div class="ms-2">
({{ member.MBTI }})
<img
role="img"
class="w-px-30 h-px-40"
:src="`/img/mbti/${member.MBTI.toLowerCase()}.png`"
alt="MBTI image"
/>
</div>
</div>
<div class="d-flex flex-sm-row align-items-center pb-2">
<i class="bx bx-id-card"></i>
<div class="ms-2">{{ member.MEMBERIDS }}</div>
</div>
<div class="d-flex flex-sm-row align-items-center pb-2">
<i class="bx bxs-phone"></i>
<div class="ms-2">{{ $common.addHyphenToPhoneNumber(member.MEMBERTEL) }}</div>
</div>
<div class="d-flex flex-sm-row align-items-center pb-2">
<i class="bx bx-calendar"></i>
<div class="ms-2">{{ $common.dateFormatter(member.MEMBERCDT) }}</div>
</div>
</div>
<div class="col-2 d-flex align-items-center">
<div>
<div>
<label class="switch"
><input
type="checkbox"
:checked="member.checked"
@click="handleRegisterMember($event, member)" />
<span class="slider round"></span
></label>
</div>
<button
class="btn-close btn-close-sm"
style="position: absolute; top: 10px; right: 10px"
@click="handleRejectMember(member)"
></button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { onMounted, ref } from 'vue';
import { useToastStore } from '@s/toastStore';
import $api from '@api';
const memberList = ref([]);
const toast = useToastStore();
const imgURL = import.meta.env.VITE_SERVER_IMG_URL;
// api
const fetchRegisterMemberList = async () => {
const { data } = await $api.get('main/registerMemberList');
if (data?.data) {
memberList.value = data.data.map(member => ({
...member,
checked: false, // checked
}));
}
};
// api
const handleRegisterMember = async (e, member) => {
e.preventDefault();
const { data } = await $api.post('main/registerMember', { memberSeq: member.MEMBERSEQ });
if (data?.data) {
member.checked = true;
toast.onToast(data.data, 's');
fetchRegisterMemberList();
}
};
// api
const handleRejectMember = async member => {
if (!confirm('해당 사원 등록을 거절하시겠습니까?')) return;
const { data } = await $api.post('main/rejectMember', { memberSeq: member.MEMBERSEQ });
if (data?.data) {
toast.onToast(data.data, 's');
fetchRegisterMemberList();
}
};
onMounted(async () => {
await fetchRegisterMemberList();
});
</script>

View File

@ -0,0 +1,114 @@
<template>
<div class="position-relative">
<div @click="togglePopover">
<slot name="trigger">
<i class="bx bxs-map"></i>
</slot>
</div>
<div
v-if="isVisible"
class="position-absolute map z-3"
>
<button
type="button"
class="btn-close popover-close"
@click="togglePopover"
></button>
<div class="card">
<div class="card-body p-1">
<KakaoMap
v-if="coordinates"
:lat="coordinates.lat"
:lng="coordinates.lng"
class="w-px-250 h-px-200"
@onLoadKakaoMap="onLoadKakaoMap"
>
<KakaoMapMarker
:lat="coordinates.lat"
:lng="coordinates.lng"
/>
</KakaoMap>
<div class="position-absolute top-50 translate-middle-y end-0 me-3 z-1 d-flex flex-column gap-1">
<button class="btn-secondary border-none" @click="zoomOut">+</button>
<button class="btn-secondary border-none" @click="zoomIn">-</button>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue';
import { KakaoMap, KakaoMapMarker } from 'vue3-kakao-maps';
const props = defineProps({
address: {
type: String,
required: true
},
isVisible: {
type: Boolean,
required: false
}
});
const emit = defineEmits(['update-visible']);
const coordinates = ref(null);
const map = ref(null);
//
const convertAddressToCoordinates = () => {
return new Promise((resolve, reject) => {
if (!window.kakao || !window.kakao.maps) {
reject(new Error('Kakao Maps not loaded'));
return;
}
const geocoder = new window.kakao.maps.services.Geocoder();
geocoder.addressSearch(props.address, (result, status) => {
if (status === window.kakao.maps.services.Status.OK) {
resolve({
lat: parseFloat(result[0].y),
lng: parseFloat(result[0].x)
});
} else {
reject(new Error('Address conversion failed'));
}
});
});
};
const togglePopover = () => {
emit('update-visible', !props.isVisible);
};
const onLoadKakaoMap = (mapRef) => {
map.value = mapRef;
};
//
const zoomIn = () => {
if (map.value) {
const level = map.value.getLevel();
map.value.setLevel(level + 1);
}
};
//
const zoomOut = () => {
if (map.value) {
const level = map.value.getLevel();
map.value.setLevel(level - 1);
}
};
//
onMounted(async () => {
coordinates.value = await convertAddressToCoordinates();
});
</script>

View File

@ -1,13 +1,12 @@
<template> <template>
<div @click="closeModal" class="modal fade" :class="{ 'show': display, 'display-block': display , 'modal-back' : display }" id="modalCenter" tabindex="-1" aria-modal="true" role="dialog"> <div @click="closeModal" class="modal fade scrollbar-none" :class="{ 'show': display , 'd-block': display , 'bg-dark bg-opacity-50' : display }" id="modalCenter" tabindex="-1" aria-modal="true" role="dialog">
<div @click.stop class="modal-dialog modal-dialog-centered" role="document"> <div @click.stop class="modal-dialog modal-dialog-centered" role="document">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<h5 class="modal-title" id="modalCenterTitle"> <h5 class="modal-title m-auto fw-bold" id="modalCenterTitle">
<slot name="title">Modal Title</slot> <slot name="title">Modal Title</slot>
</h5> </h5>
<button type="button" class="btn-close" @click="closeModal" aria-label="Close"></button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<slot name="body">Modal body</slot> <slot name="body">Modal body</slot>
@ -21,26 +20,31 @@
</template> </template>
<script setup> <script setup>
const prop = defineProps({ const prop = defineProps({
display : { display : {
type: Boolean, type: Boolean,
default: false, default: false,
required: true, required: true,
}, },
create: {
type: Boolean,
default: false,
}
}); });
const emit = defineEmits(['close']); const emit = defineEmits(['close' , 'reset']);
const closeModal = () => { const closeModal = () => {
if (prop.create) {
emit('reset');
}
emit('close', false); emit('close', false);
}; };
</script> </script>
<style>
.modal-back {
background: rgba(0, 0, 0, 0.5);
}
</style>

View File

@ -1,7 +1,7 @@
<template> <template>
<div <div
v-if="toastStore.toastModal" v-if="toastStore.toastModal"
:class="['bs-toast toast m-2 fade show', toastClass]" :class="['bs-toast toast toast-placement-ex m-2 fade show', toastClass]"
role="alert" role="alert"
aria-live="assertive" aria-live="assertive"
aria-atomic="true" aria-atomic="true"
@ -35,24 +35,3 @@ const toastClass = computed(() => {
return toastStore.toastType === 'e' ? 'bg-danger' : 'bg-success'; // red, blue return toastStore.toastType === 'e' ? 'bg-danger' : 'bg-success'; // red, blue
}); });
</script> </script>
<style scoped>
.bs-toast {
position: fixed;
bottom: 20px; /* 화면 하단에 위치 */
right: 20px; /* 오른쪽에 위치 */
z-index: 2000; /* 충분히 높은 값으로 설정 */
max-width: 300px; /* 최대 너비 제한 */
opacity: 1;
transition: opacity 0.5s ease-in-out;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); /* 그림자 추가 */
}
.bg-primary {
background-color: #007bff !important; /* 성공 색상 */
}
.bg-danger {
background-color: #ff3e1d !important; /* 에러 색상 */
}
</style>

View File

@ -0,0 +1,125 @@
<template>
<div v-if="isOpen" class="vac-modal-dialog" @click.self="closeModal">
<div class="vac-modal-content">
<div class="vac-modal-header">
<h5 class="vac-grant-modal-title">To. {{ targetUser.MEMBERNAM }} 🎁</h5>
<button class="close-btn" @click="closeModal"></button>
</div>
<div class="vac-modal-body">
<p class="vac-modal-text">선물할 연차 개수를 선택해 주세요.</p>
<div class="count-container">
<button @click="decreaseCount" :disabled="grantCount < 2" class="count-btn">-</button>
<span class="count-value">{{ grantCount }}</span>
<button @click="increaseCount" :disabled="grantCount >= availableQuota" class="count-btn">+</button>
</div>
<div class="custom-button-container">
<button class="custom-button" @click="saveVacationGrant" :disabled="grantCount === 0 || isGiftButtonDisabled">
<i class="bx bx-gift"></i>
</button>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, defineProps, defineEmits, watch, onMounted, computed } from "vue";
import axios from "@api";
import { useToastStore } from '@s/toastStore';
import { useUserInfoStore } from "@s/useUserInfoStore";
const userStore = useUserInfoStore();
const toastStore = useToastStore();
const props = defineProps({
isOpen: Boolean,
targetUser: Object,
remainingVacationData: Object,
});
const emit = defineEmits(["close", "updateVacation"]);
const grantCount = ref(0);
const maxQuota = 2;
const sentCount = ref(0);
const availableQuota = ref(2);
const myUserId = computed(() => userStore.user.id);
const myRemainingQuota = computed(() => {
return props.remainingVacationData?.[myUserId.value] ?? 0;
});
const isGiftButtonDisabled = computed(() => {
return myRemainingQuota.value < 0;
});
//
const fetchSentVacationCount = async () => {
try {
const payload = { receiverId: props.targetUser.MEMBERSEQ };
const response = await axios.get("vacation/sent", { params: payload });
sentCount.value = response.data.data[0]?.count || 0;
availableQuota.value = Math.max(maxQuota - sentCount.value, 0);
grantCount.value = availableQuota.value;
} catch (error) {
availableQuota.value = maxQuota;
grantCount.value = maxQuota;
}
};
//
const increaseCount = () => {
if (grantCount.value < availableQuota.value) {
grantCount.value++;
}
};
//
const decreaseCount = () => {
if (grantCount.value > 0) {
grantCount.value--;
}
};
//
const saveVacationGrant = async () => {
try {
const payload = [{
date: new Date().toISOString().split("T")[0],
type: "700103",
receiverId: props.targetUser.MEMBERSEQ,
count: grantCount.value,
}];
const response = await axios.post("vacation", payload);
if (response.data?.status === "OK") {
toastStore.onToast('연차가 선물되었습니다.', 's');
await fetchSentVacationCount();
emit("updateVacation");
closeModal();
} else {
toastStore.onToast(' 연차 선물 중 오류가 발생했습니다.', 'e');
}
} catch (error) {
toastStore.onToast(' 연차 선물 실패!!.', 'e');
}
};
//
const closeModal = () => {
emit("close");
};
watch(() => props.isOpen, async (newVal) => {
if (newVal && props.targetUser?.MEMBERSEQ) {
await fetchSentVacationCount();
}
});
watch(() => props.targetUser, async (newUser) => {
if (newUser?.MEMBERSEQ) {
await fetchSentVacationCount();
}
}, { deep: true });
onMounted(async () => {
if (props.isOpen && props.targetUser?.MEMBERSEQ) {
await fetchSentVacationCount();
}
});
</script>
<style scoped>
</style>

View File

@ -0,0 +1,126 @@
<template>
<div v-if="isOpen" class="vac-modal-dialog" @click.self="closeModal">
<div class="vac-modal-content p-5 modal-scroll">
<h5 class="vac-modal-title">📅 연차 (누적 개수)</h5>
<button class="close-btn" @click="closeModal"></button>
<!-- 연차 목록 -->
<div class="vac-modal-body" v-if="mergedVacations.length > 0">
<ol class="list-group-numbered px-0 mt-4">
<li
v-for="(vac, index) in mergedVacations"
:key="vac._expandIndex"
class="vacation-item"
>
<span :class="vac.category === 'used' ? 'fw-bold text-danger me-2' : 'fw-bold text-primary me-2'">
{{ vac.category === 'used' ? '-' : '+' }}
</span>
<span
:style="{ color: userColors[vac.senderId || vac.receiverId] || '#000' }"
>
{{ formatDate(vac.date) }}
</span>
<span v-if="vac.category === 'used'" class="fw-bold text-dark ms-1">
( {{ usedVacationIndexMap[vac._expandIndex] }} )
</span>
</li>
</ol>
</div>
<!-- 연차 데이터 없음 -->
<p v-else class="text-sm-center mt-10 text-gray vac-modal-title">
🚫 연차 내역이 없습니다.
</p>
</div>
</div>
</template>
<script setup>
import { defineProps, defineEmits, computed } from "vue";
import { formatDate } from '@/common/formattedDate.js';
const props = defineProps({
isOpen: Boolean,
myVacations: {
type: Array,
default: () => [],
},
receivedVacations: {
type: Array,
default: () => [],
},
userColors: {
type: Object,
default: () => ({}),
},
});
const emit = defineEmits(["close"]);
// (,)
let globalCounter = 0;
const usedVacations = computed(() => {
const data = props.myVacations.flatMap((v) => {
const count = v.used_quota
return Array.from({ length: Math.ceil(count) }, (_, i) => ({
...v,
category: "used",
code: v.LOCVACTYP,
used_quota: count, //
_expandIndex: globalCounter++,
}));
});
return data;
});
//
const receivedVacations = computed(() => {
const data = props.receivedVacations.flatMap((v) => {
const count = v.received_quota ?? 1;
return Array.from({ length: Math.ceil(count) }, (_, i) => ({
...v,
category: "received",
_expandIndex: globalCounter++,
}));
});
return data;
});
//
const sortedUsedVacationsAsc = computed(() => {
return [...usedVacations.value].sort((a, b) => {
return new Date(a.date) - new Date(b.date) || (a._expandIndex - b._expandIndex);
});
});
// type === "700103" +1 +0.5
const usedVacationIndexMap = computed(() => {
let cumulative = 0;
const map = {};
sortedUsedVacationsAsc.value.forEach((item) => {
const increment = item.type === "700103" ? 1 : 0.5;
cumulative += increment;
map[item._expandIndex] = cumulative;
});
return map;
});
// merged (Used + Received) ( )
const mergedVacations = computed(() => {
const all = [...usedVacations.value, ...receivedVacations.value];
// +
all.sort((a, b) => {
const dateDiff = new Date(b.date) - new Date(a.date);
if (dateDiff !== 0) return dateDiff;
return b._expandIndex - a._expandIndex;
});
return all;
});
//
const closeModal = () => {
emit("close");
};
</script>
<style scoped>
</style>

View File

@ -66,51 +66,55 @@
const props = defineProps({ const props = defineProps({
currentPage: { currentPage: {
type: Number, type: Number,
required: true required: false
}, },
pages: { pages: {
type: Number, type: Number,
required: true required: false
}, },
prePage: { prePage: {
type: Number, type: Number,
required: true required: false
}, },
nextPage: { nextPage: {
type: Number, type: Number,
required: true required: false
}, },
isFirstPage: { isFirstPage: {
type: Boolean, type: Boolean,
required: true required: false
}, },
isLastPage: { isLastPage: {
type: Boolean, type: Boolean,
required: true required: false
}, },
hasPreviousPage: { hasPreviousPage: {
type: Boolean, type: Boolean,
required: true required: false
}, },
hasNextPage: { hasNextPage: {
type: Boolean, type: Boolean,
required: true required: false
}, },
navigatePages: { navigatePages: {
type: Number, type: Number,
required: true required: false
}, },
navigatepageNums: { navigatepageNums: {
type: Array, type: Array,
required: true required: false
}, },
navigateFirstPage: { navigateFirstPage: {
type: Number, type: Number,
required: true required: false
}, },
navigateLastPage: { navigateLastPage: {
type: Number, type: Number,
required: true required: false
},
PageData:{
type:Array,
required:false,
} }
}); });

View File

@ -0,0 +1,378 @@
<template>
<SearchBar @update:data="search" />
<div class="d-flex align-items-center">
<CategoryBtn :lists="yearCategory" @update:data="selectedCategory = $event" />
<WriteBtn class="mt-2 ms-auto" @click="openCreateModal" />
</div>
<!-- 프로젝트 목록 -->
<div class="mt-4">
<div v-if="projectStore.projectList.length === 0" class="text-center">
<p class="text-muted mt-4">등록된 프로젝트가 없습니다.</p>
</div>
<div v-for="post in projectStore.projectList" :key="post.PROJCTSEQ">
<ProjectCard
:title="post.PROJCTNAM"
:description="post.PROJCTDES ?? ''"
:strdate="post.PROJCTSTR"
:enddate="post.PROJCTEND"
:address="post.PROJCTARR"
:addressdtail="post.PROJCTDTL"
:addressZip="post.PROJCTZIP"
:projctSeq="post.PROJCTSEQ"
:projctCol="post.PROJCTCOL"
:projctColor="post.projctcolor"
:projctCreatorId="post.PROJCTCMB"
:resetUserSelection="resetUserSelection"
:searchParams="{ text: searchText, year: selectedYear }"
@update="handleProjectUpdate"
/>
</div>
</div>
<!-- 등록 모달 -->
<form @reset.prevent="formReset">
<CenterModal :display="isCreateModalOpen" @close="closeCreateModal" :create="true" @reset="formReset">
<template #title> 프로젝트 등록 </template>
<template #body>
<FormInput
title="프로젝트명"
name="name"
:is-essential="true"
:is-alert="nameAlert"
:modelValue="name"
@update:alert="nameAlert = $event"
@update:modelValue="name = $event"
/>
<FormSelect
title="컬러"
name="color"
:is-essential="true"
:is-label="true"
:is-common="true"
:is-color="true"
:value="color"
:data="colorList"
@update:data="color = $event"
:is-alert="colorAlert"
/>
<div class="mb-2 row">
<label class="col-md-2 col-form-label">
참여자
</label>
<div class="col-md-10">
<UserList
ref="userListRef"
@user-list-update="handleUserListUpdate"
class="m-0"
/>
</div>
</div>
<div @click="openStartDatePicker">
<FormInput
title="시작 일"
name="startDay"
:type="'date'"
:is-alert="startDayAlert"
:is-essential="true"
:modelValue="startDay"
v-model="startDay"
ref="startDateInput"
/>
</div>
<div @click="openEndDatePicker">
<FormInput
title="종료 일"
name="endDay"
:type="'date'"
:modelValue="endDay"
:min="startDay"
@update:modelValue="endDay = $event"
ref="endDateInput"
/>
</div>
<FormInput
title="설명"
name="description"
:modelValue="description"
@update:modelValue="description = $event"
/>
<ArrInput
title="주소"
name="address"
:isEssential="true"
:is-row="true"
:is-alert="addressAlert"
:modelValue="addressData"
@update:data="handleAddressUpdate"
@update:alert="addressAlert = $event"
/>
</template>
<template #footer>
<BackButton type="reset" @click="closeCreateModal" />
<SaveButton @click="handleCreate" />
</template>
</CenterModal>
</form>
</template>
<script setup>
import { computed, ref, watch, onMounted, inject, nextTick } from 'vue';
import SearchBar from '@c/search/SearchBar.vue';
import ProjectCard from '@c/list/ProjectCard.vue';
import CategoryBtn from '@/components/category/CategoryBtn.vue';
import WriteBtn from '@c/button/WriteBtn.vue';
import CenterModal from '@c/modal/CenterModal.vue';
import FormSelect from '@c/input/FormSelect.vue';
import FormInput from '@c/input/FormInput.vue';
import ArrInput from '@c/input/ArrInput.vue';
import UserList from '@c/user/UserList.vue';
import commonApi, { refreshColorList } from '@/common/commonApi';
import { useToastStore } from '@s/toastStore';
import { useUserInfoStore } from '@/stores/useUserInfoStore';
import { useProjectStore } from '@/stores/useProjectStore';
import $api from '@api';
import SaveButton from '@c/button/SaveBtn.vue';
import BackButton from '@c/button/BackBtn.vue';
const toastStore = useToastStore();
const userStore = useUserInfoStore();
const projectStore = useProjectStore();
//
const user = ref(null);
const selectedCategory = ref(null);
const searchText = ref('');
const userListRef = ref(null);
const resetUserSelection = ref(false);
// dayjs
const dayjs = inject('dayjs');
// YYYY-MM-DD
const today = dayjs().format('YYYY-MM-DD');
//
const isCreateModalOpen = ref(false);
const name = ref('');
const color = ref('0');
const startDay = ref(today);
const endDay = ref('');
const description = ref('');
const nameAlert = ref(false);
const colorAlert = ref(false);
const addressAlert = ref(false);
const startDayAlert = ref(false);
const startDateInput = ref(null);
const endDateInput = ref(null);
// DOM
let startInputElement = null;
let endInputElement = null;
const openStartDatePicker = () => {
if (startInputElement) {
startInputElement.showPicker();
}
};
const openEndDatePicker = () => {
if (endInputElement) {
endInputElement.showPicker();
}
};
const addressData = ref({
postcode: '',
address: '',
detailAddress: ''
});
//
const selectedUsers = ref({
activeUsers: [],
disabledUsers: []
});
// UserList
const handleUserListUpdate = (userLists) => {
selectedUsers.value = userLists;
};
// API
const { yearCategory, colorList } = commonApi({
loadColor: true,
colorType: 'YNP',
loadYearCategory: true,
});
//
const search = async searchKeyword => {
searchText.value = searchKeyword.trim();
await getProjectList();
};
const selectedYear = computed(() => {
if (!selectedCategory.value || selectedCategory.value === 900101) {
return null;
}
// category label
return yearCategory.value.find(item => item.value === selectedCategory.value)?.label || null;
});
//
const getProjectList = async () => {
await projectStore.getProjectList(searchText.value, selectedYear.value, 'false');
};
//
watch(selectedCategory, async () => {
await getProjectList();
});
//
const openCreateModal = async () => {
const updatedColors = await refreshColorList('YNP');
if (updatedColors && updatedColors.length > 0) {
color.value = updatedColors[0].value;
}
isCreateModalOpen.value = true;
};
const closeCreateModal = () => {
isCreateModalOpen.value = false;
resetUserSelection.value = !resetUserSelection.value;
};
const formReset = () => {
name.value = '';
if (colorList.value && colorList.value.length > 0) {
color.value = colorList.value[0].value;
}
addressData.value = {
postcode: '',
address: '',
detailAddress: ''
};
startDay.value = today;
endDay.value = '';
description.value = '';
nameAlert.value = false;
addressAlert.value = false;
startDayAlert.value = false;
selectedUsers.value = {
activeUsers: [],
disabledUsers: []
};
if (userListRef.value) {
userListRef.value.resetSelection();
}
};
// ::
const handleAddressUpdate = (data) => {
addressData.value = data;
};
//
watch(startDay, (newStartDate) => {
if (new Date(newStartDate) > new Date(endDay.value)) {
endDay.value = '';
}
});
const handleProjectUpdate = async (params) => {
if (params) {
await projectStore.getProjectList(params.text, params.year, 'false');
} else {
await projectStore.getProjectList(searchText.value, selectedYear.value, 'false');
}
await projectStore.getMemberProjects();
//
const updatedColors = await refreshColorList('YNP');
// ()
if (updatedColors && updatedColors.length > 0) {
color.value = updatedColors[0].value;
}
};
//
const handleCreate = async () => {
nameAlert.value = name.value.trim() === '';
startDayAlert.value = startDay.value.trim() === '';
addressAlert.value = addressData.value.address.trim() === '';
if (!colorList.value || colorList.value.length === 0) {
colorAlert.value = true;
}
if (nameAlert.value || startDayAlert.value || addressAlert.value || colorAlert.value) {
return;
}
//
const disabledMemberSeqs = selectedUsers.value.disabledUsers.map(user => user.MEMBERSEQ);
const response = await $api.post('project/insert', {
projctNam: name.value,
projctCol: color.value,
projctStr: startDay.value,
projctEnd: endDay.value || null,
projctDes: description.value || null,
projctArr: addressData.value.address,
projctDtl: addressData.value.detailAddress,
projctZip: addressData.value.postcode,
projctCmb: user.value.id,
disabledMembers: disabledMemberSeqs
});
if (response.status === 200) {
toastStore.onToast('프로젝트가 등록되었습니다.', 's');
colorList.value = colorList.value.filter(c => c.value !== color.value);
formReset();
await getProjectList();
await projectStore.getMemberProjects();
closeCreateModal();
resetUserSelection.value = !resetUserSelection.value;
}
};
onMounted(async () => {
await getProjectList();
await userStore.userInfo();
user.value = userStore.user;
if (startDateInput.value) {
// FormInput input
startInputElement = startDateInput.value.$el.querySelector('input[type="date"]');
}
if (endDateInput.value) {
endInputElement = endDateInput.value.$el.querySelector('input[type="date"]');
}
});
</script>

View File

@ -1,25 +1,63 @@
<template> <template>
<form @submit.prevent="search">
<div class="input-group mb-3 d-flex"> <div class="input-group mb-3 d-flex">
<input type="text" class="form-control" placeholder="Search" @change="search" /> <input type="text" class="form-control" placeholder="Search" v-model="searchQuery" @input="preventLeadingSpace" />
<button type="button" class="btn btn-primary"><i class="bx bx-search bx-md"></i></button> <button type="submit" class="btn btn-primary">
<i class="bx bx-search bx-md"></i>
</button>
</div> </div>
</form>
</template> </template>
<script setup> <script setup>
import { ref, watch } from 'vue';
const props = defineProps({ const props = defineProps({
maxlength: { maxlength: {
type: Number, type: Number,
default: 30, default: 30,
required: false, required: false,
}, },
initKeyword: {
type: String,
},
}); });
const emits = defineEmits(['update:data']); const emits = defineEmits(['update:data']);
const search = function (event) { const searchQuery = ref('');
//Type Number maxlength
if (event.target.value.length > props.maxlength) { watch(
event.target.value = event.target.value.slice(0, props.maxlength); () => props.initKeyword,
(newVal, oldVal) => {
searchQuery.value = newVal;
},
);
// ( or )
const search = () => {
const trimmedQuery = searchQuery.value.trimStart();
if (trimmedQuery === '') {
emits('update:data', '');
return;
} }
emits('update:data', event.target.value); if (trimmedQuery.length < 2) {
alert('검색어는 최소 2글자 이상 입력해주세요.');
searchQuery.value = '';
return;
}
//
if (trimmedQuery.length > props.maxlength) {
searchQuery.value = trimmedQuery.slice(0, props.maxlength);
} else {
searchQuery.value = trimmedQuery;
}
emits('update:data', searchQuery.value);
};
//
const preventLeadingSpace = () => {
searchQuery.value = searchQuery.value.trimStart();
}; };
</script> </script>

View File

@ -0,0 +1,206 @@
<template>
<div class="col-xl-12">
<UserFormInput
title="아이디"
name="id"
:is-alert="idAlert"
:is-essential="true"
:useInputGroup="true"
@update:data="handleIdChange"
:value="id"
:disabled="resetForm"
/>
<template v-if="!resetForm">
<UserFormInput
title="생년월일"
name="birth"
:type="'date'"
:is-essential="true"
:is-alert="birthAlert"
@update:data="birth = $event"
@update:alert="birthAlert = $event"
:value="birth"
/>
<FormSelect
title="비밀번호 힌트"
name="pwhint"
:is-essential="true"
:is-row="false"
:is-label="true"
:is-common="true"
:data="pwhintList"
@update:data="pwhint = $event"
/>
<UserFormInput
title="답변"
name="pwhintRes"
:is-essential="true"
:is-alert="pwhintResAlert"
@update:data="pwhintRes = $event"
@update:alert="pwhintResAlert = $event"
:value="pwhintRes"
/>
<div class="d-flex gap-2 mt-7 mb-3">
<BackBtn class=" w-50" @click="handleback"/>
<SaveBtn class="w-50" @click="handleSubmit" />
</div>
<p v-if="userCheckMsg" class="invalid-feedback d-block mb-0">{{ userCheckMsg }}</p>
</template>
</div>
<div v-if="resetForm" class="mt-4">
<UserFormInput
title="새 비밀번호"
name="pw"
type="password"
:isEssential="true"
:is-alert="passwordAlert"
@update:data="password = $event"
@update:alert="passwordAlert = $event"
:value="password"
/>
<UserFormInput
title="비밀번호 확인"
name="pwch"
type="password"
:isEssential="true"
:is-alert="passwordcheckAlert"
@update:data="passwordcheck = $event"
@update:alert="passwordcheckAlert = $event"
@input="checkPw"
:value="passwordcheck"
/>
<span v-if="passwordcheckError" class="invalid-feedback d-block">{{ passwordcheckError }}</span>
<div class="d-flex gap-2 mt-7 mb-3">
<BackBtn class=" w-50" @click="handleback"/>
<SaveBtn class="w-50" @click="handleNewPassword" />
</div>
<p v-if="pwErrMsg" class="invalid-feedback d-block mb-0">{{ pwErrMsg }}</p>
</div>
</template>
<script setup>
import { ref, watch } from 'vue';
import $api from '@api';
import commonApi from '@/common/commonApi';
import { useRouter } from 'vue-router';
import { useToastStore } from '@s/toastStore';
import UserFormInput from '@c/input/UserFormInput.vue';
import FormSelect from '../input/FormSelect.vue';
import BackBtn from '@c/button/BackBtn.vue';
import SaveBtn from '../button/SaveBtn.vue';
const router = useRouter();
const toastStore = useToastStore();
const id = ref('');
const birth = ref('');
const pwhint = ref('');
const pwhintRes = ref('');
const userCheckMsg = ref("");
const pwErrMsg = ref("");
const idAlert = ref(false);
const birthAlert = ref(false);
const pwhintResAlert = ref(false);
const resetForm = ref(false);
const password = ref('');
const passwordcheck = ref('');
const passwordcheckError = ref('');
const passwordAlert = ref(false);
const passwordcheckAlert = ref(false);
const passwordMismatch = ref(false);
const { pwhintList } = commonApi({
loadPwhint: true,
});
const handleIdChange = value => {
id.value = value;
idAlert.value = false;
};
const handleback = () => {
router.push('/login');
}
//
watch([password, passwordcheck], () => {
if (passwordcheck.value !== '') {
checkPw();
}
});
// , , , member input
const handleSubmit = async () => {
userCheckMsg.value = '';
idAlert.value = id.value.trim() === '';
pwhintResAlert.value = pwhintRes.value.trim() === '';
birthAlert.value = birth.value.trim() === '';
if (idAlert.value || pwhintResAlert.value || birthAlert.value) {
return;
}
const response = await $api.post('user/pwReset', {
id: id.value,
birth: birth.value,
pwhint: pwhint.value,
pwhintRes: pwhintRes.value,
});
if (response.status === 200 && response.data.data === true) {
resetForm.value = true;
} else {
userCheckMsg.value = '입력하신 정보와 일치하는 회원이 없습니다.';
return;
}
};
const checkPw = () => {
if (password.value !== passwordcheck.value) {
passwordcheckError.value = '비밀번호가 일치하지 않습니다.';
passwordMismatch.value = true;
} else {
passwordcheckError.value = '';
passwordMismatch.value = false;
}
};
//
const handleNewPassword = async () => {
pwErrMsg.value = '';
passwordAlert.value = password.value.trim() === '';
passwordcheckAlert.value = passwordcheck.value.trim() === '';
checkPw();
if (passwordAlert.value || passwordcheckAlert.value || passwordMismatch.value) {
return;
}
const checkResponse = await $api.post('user/checkPassword', {
id: id.value,
password: password.value
});
if (checkResponse.data.data === false) {
pwErrMsg.value = '기존 비밀번호와 동일한 비밀번호로 변경할 수 없습니다.';
return;
}
const response = await $api.patch('user/pwNew', {
id: id.value,
password: password.value
});
if (response.status === 200 && response.data.data === true) {
toastStore.onToast('비밀번호가 재설정 되었습니다.', 's');
router.push('/login');
}
};
</script>

View File

@ -1,14 +1,7 @@
<template> <template>
<form @submit.prevent="handleSubmit"> <form @submit.prevent="handleSubmit">
<div class="col-xl-12"> <div class="col-xl-12">
<UserFormInput <UserFormInput title="아이디" name="id" :is-alert="idAlert" :useInputGroup="true" @update:data="handleIdChange" :value="id" />
title="아이디"
name="id"
:is-alert="idAlert"
:useInputGroup="true"
@update:data="handleIdChange"
:value="id"
/>
<UserFormInput <UserFormInput
title="비밀번호" title="비밀번호"
@ -21,15 +14,16 @@
<div class="d-grid gap-2 mt-7 mb-5"> <div class="d-grid gap-2 mt-7 mb-5">
<button type="submit" class="btn btn-primary">로그인</button> <button type="submit" class="btn btn-primary">로그인</button>
<p v-if="errorMessage" class="invalid-feedback d-block mb-0">{{ errorMessage }}</p>
</div> </div>
<div class="mb-3 d-flex justify-content-around"> <div class="mb-3 d-flex justify-content-around">
<div> <div>
<input type="checkbox" class="form-check-input" id="rememberCheck" /> <input type="checkbox" class="form-check-input" id="rememberCheck" v-model="remember" />
<label class="form-check-label fw-bold" for="rememberCheck">&nbsp;자동로그인</label> <label class="form-check-label fw-bold" for="rememberCheck">&nbsp;자동로그인</label>
</div> </div>
<RouterLink class="text-dark fw-bold" to="/register">등록신청</RouterLink> <RouterLink class="text-dark fw-bold" to="/register">등록신청</RouterLink>
<RouterLink class="text-dark fw-bold" to="/pw">비밀번호 찾기</RouterLink> <RouterLink class="text-dark fw-bold" to="/pw">비밀번호 재설정</RouterLink>
</div> </div>
</div> </div>
</form> </form>
@ -40,11 +34,16 @@
import router from '@/router'; import router from '@/router';
import { ref } from 'vue'; import { ref } from 'vue';
import UserFormInput from '@c/input/UserFormInput.vue'; import UserFormInput from '@c/input/UserFormInput.vue';
import { useUserInfoStore } from '@/stores/useUserInfoStore';
const id = ref(''); const id = ref('');
const password = ref(''); const password = ref('');
const idAlert = ref(false); const idAlert = ref(false);
const passwordAlert = ref(false); const passwordAlert = ref(false);
const remember = ref(false);
const errorMessage = ref("");
const userStore = useUserInfoStore();
const handleIdChange = value => { const handleIdChange = value => {
id.value = value; id.value = value;
@ -57,22 +56,31 @@
}; };
const handleSubmit = async () => { const handleSubmit = async () => {
errorMessage.value = '';
idAlert.value = id.value.trim() === '';
passwordAlert.value = password.value.trim() === '';
try { if (idAlert.value || passwordAlert.value) {
const response = await $api.post('user/login', { return;
}
$api.post('user/login', {
loginId: id.value, loginId: id.value,
password: password.value, password: password.value,
remember: false, remember: remember.value,
}); }, { headers: { isLogin: true } })
.then(async res => {
//
if (res.data.code > 10000) {
//
errorMessage.value = res.data.message;
return;
}
if (response.status === 200) { //
console.log('로그인 성공', response.data); await userStore.userInfo();
router.push('/'); router.push('/');
} })
} catch (error) {
console.error('로그인 실패', error);
}
}; };
</script> </script>

View File

@ -17,7 +17,7 @@
/> />
</svg> </svg>
</span> </span>
<h2>{{ title }}</h2> <h3 class="fw-bold">{{ title }}</h3>
</div> </div>
</template> </template>

View File

@ -5,7 +5,7 @@
for="profilePic" for="profilePic"
class="rounded-circle m-auto ui-bg-cover position-relative cursor-pointer " class="rounded-circle m-auto ui-bg-cover position-relative cursor-pointer "
id="profileLabel" id="profileLabel"
style="width: 100px; height: 100px; background-image: url(public/img/avatars/default-Profile.jpg); background-repeat: no-repeat;" style="width: 100px; height: 100px; background-image: url(img/avatars/default-Profile.jpg); background-repeat: no-repeat; background-size: cover;"
> >
</label> </label>
@ -25,6 +25,7 @@
@update:alert="idAlert = $event" @update:alert="idAlert = $event"
@blur="checkIdDuplicate" @blur="checkIdDuplicate"
:value="id" :value="id"
@keypress="noSpace"
/> />
<span v-if="idError" class="invalid-feedback d-block">{{ idError }}</span> <span v-if="idError" class="invalid-feedback d-block">{{ idError }}</span>
@ -37,7 +38,9 @@
@update:data="password = $event" @update:data="password = $event"
@update:alert="passwordAlert = $event" @update:alert="passwordAlert = $event"
:value="password" :value="password"
@keypress="noSpace"
/> />
<span v-if="passwordError" class="invalid-feedback d-block">{{ passwordError }}</span>
<UserFormInput <UserFormInput
title="비밀번호 확인" title="비밀번호 확인"
@ -47,8 +50,8 @@
:is-alert="passwordcheckAlert" :is-alert="passwordcheckAlert"
@update:data="passwordcheck = $event" @update:data="passwordcheck = $event"
@update:alert="passwordcheckAlert = $event" @update:alert="passwordcheckAlert = $event"
@blur="checkPw"
:value="passwordcheck" :value="passwordcheck"
@keypress="noSpace"
/> />
<span v-if="passwordcheckError" class="invalid-feedback d-block">{{ passwordcheckError }}</span> <span v-if="passwordcheckError" class="invalid-feedback d-block">{{ passwordcheckError }}</span>
@ -58,9 +61,11 @@
:is-essential="true" :is-essential="true"
:is-row="false" :is-row="false"
:is-label="true" :is-label="true"
:is-common="true"
:data="pwhintList" :data="pwhintList"
@update:data="pwhint = $event" @update:data="pwhint = $event"
/> />
<UserFormInput <UserFormInput
title="답변" title="답변"
name="pwhintRes" name="pwhintRes"
@ -80,6 +85,7 @@
@update:data="name = $event" @update:data="name = $event"
@update:alert="nameAlert = $event" @update:alert="nameAlert = $event"
:value="name" :value="name"
@keypress="noSpace"
class="me-2 w-50" class="me-2 w-50"
/> />
@ -90,11 +96,14 @@
:is-row="false" :is-row="false"
:is-label="true" :is-label="true"
:is-common="true" :is-common="true"
:is-color="true"
:data="colorList" :data="colorList"
@update:data="color = $event" @update:data="handleColorUpdate"
:is-alert="colorAlert"
class="w-50" class="w-50"
/> />
</div> </div>
<span v-if="colorError" class="w-50 ps-1 ms-auto invalid-feedback d-block">{{ colorError }}</span>
<div class="d-flex"> <div class="d-flex">
<UserFormInput <UserFormInput
@ -116,6 +125,7 @@
:is-row="false" :is-row="false"
:is-label="true" :is-label="true"
:is-common="true" :is-common="true"
:is-mbti="true"
:data="mbtiList" :data="mbtiList"
@update:data="mbti = $event" @update:data="mbti = $event"
class="w-50" class="w-50"
@ -130,6 +140,7 @@
@update:data="handleAddressUpdate" @update:data="handleAddressUpdate"
@update:alert="addressAlert = $event" @update:alert="addressAlert = $event"
:value="address" :value="address"
:disabled="true"
/> />
<UserFormInput <UserFormInput
@ -137,11 +148,14 @@
name="phone" name="phone"
:isEssential="true" :isEssential="true"
:is-alert="phoneAlert" :is-alert="phoneAlert"
@update:data="phone = $event"
@update:alert="phoneAlert = $event" @update:alert="phoneAlert = $event"
@blur="checkPhoneDuplicate"
:maxlength="11" :maxlength="11"
:value="phone" :value="phone"
@keypress="onlyNumber"
@input="inputEvent"
/> />
<span v-if="phoneError" class="invalid-feedback d-block">{{ phoneError }}</span>
<div class="d-flex mt-5"> <div class="d-flex mt-5">
<RouterLink type="button" class="btn btn-secondary me-2 w-50" to="/login">취소</RouterLink> <RouterLink type="button" class="btn btn-secondary me-2 w-50" to="/login">취소</RouterLink>
@ -152,8 +166,9 @@
</template> </template>
<script setup> <script setup>
import { ref, onMounted } from 'vue'; import { ref, watch } from 'vue';
import $api from '@api'; import $api from '@api';
import commonApi from '@/common/commonApi';
import UserFormInput from '@c/input/UserFormInput.vue'; import UserFormInput from '@c/input/UserFormInput.vue';
import FormSelect from '@c/input/FormSelect.vue'; import FormSelect from '@c/input/FormSelect.vue';
import ArrInput from '@c/input/ArrInput.vue'; import ArrInput from '@c/input/ArrInput.vue';
@ -161,8 +176,6 @@
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import { useToastStore } from '@s/toastStore'; import { useToastStore } from '@s/toastStore';
const pwhintList = ['현재 살고 있는 동네', '가장 기억에 남는 책', '좋아하는 음식'];
const router = useRouter(); const router = useRouter();
const profile = ref(null); const profile = ref(null);
@ -171,9 +184,9 @@
const id = ref(''); const id = ref('');
const idError = ref(''); const idError = ref('');
const password = ref(''); const password = ref('');
const passwordError = ref('');
const passwordcheck = ref(''); const passwordcheck = ref('');
const passwordcheckError = ref(''); const passwordcheckError = ref('');
const pwhint = ref(0);
const pwhintRes = ref(''); const pwhintRes = ref('');
const name = ref(''); const name = ref('');
const birth = ref(''); const birth = ref('');
@ -181,25 +194,33 @@
const detailAddress = ref(''); const detailAddress = ref('');
const postcode = ref(''); // const postcode = ref(''); //
const phone = ref(''); const phone = ref('');
const colorList = ref([]); const phoneError = ref('');
const mbtiList = ref([]);
const color = ref(''); // color const color = ref(''); // color
const colorError = ref('');
const mbti = ref(''); // MBTI const mbti = ref(''); // MBTI
const pwhint = ref(''); // pwhint
const profilAlert = ref(false); const profilAlert = ref(false);
const idAlert = ref(false); const idAlert = ref(false);
const idErrorAlert = ref(false); const idErrorAlert = ref(false);
const passwordAlert = ref(false); const passwordAlert = ref(false);
const passwordErrorAlert = ref(false);
const passwordcheckAlert = ref(false); const passwordcheckAlert = ref(false);
const passwordcheckErrorAlert = ref(false); // const passwordcheckErrorAlert = ref(false); //
const pwhintResAlert = ref(false); const pwhintResAlert = ref(false);
const nameAlert = ref(false); const nameAlert = ref(false);
const colorAlert = ref(false);
const birthAlert = ref(false); const birthAlert = ref(false);
const addressAlert = ref(false); const addressAlert = ref(false);
const phoneAlert = ref(false); const phoneAlert = ref(false);
const phoneErrorAlert = ref(false);
const colorErrorAlert = ref(false);
const toastStore = useToastStore(); const toastStore = useToastStore();
const noSpace = (e) => {
if (e.key === ' ') e.preventDefault();
};
// //
const profileValid = (size, type) => { const profileValid = (size, type) => {
@ -229,7 +250,7 @@
// , // ,
if (!profileValid(file.size, file.type)) { if (!profileValid(file.size, file.type)) {
e.target.value = ''; e.target.value = '';
profileLabel.style.backgroundImage = 'url("public/img/avatars/default-Profile.jpg")'; profileLabel.style.backgroundImage = 'url("img/avatars/default-Profile.jpg")';
return false; return false;
} }
@ -239,10 +260,16 @@
profile.value = file; profile.value = file;
}; };
// //
const checkIdDuplicate = async () => { const checkIdDuplicate = async () => {
const response = await $api.get(`/user/checkId?memberIds=${id.value}`); //
if (id.value.length < 4) {
idError.value = '아이디는 4자리 이상이어야 합니다.';
idErrorAlert.value = true;
return;
}
const response = await $api.get(`/user/checkId?memberIds=${id.value}`);
if (!response.data.data) { if (!response.data.data) {
idErrorAlert.value = true; idErrorAlert.value = true;
idError.value = '이미 사용 중인 아이디입니다.'; idError.value = '이미 사용 중인 아이디입니다.';
@ -252,48 +279,105 @@
} }
}; };
const Colors = async () => { //
const response = await $api.get('/user/color'); const checkPhoneDuplicate = async () => {
colorList.value = response.data.data.map(item => ({ const response = await $api.get(`/user/checkPhone?memberTel=${phone.value}`);
label: item.CMNCODNAM,
value: item.CMNCODVAL if (!response.data.data) {
})); phoneErrorAlert.value = true;
phoneError.value = '이미 사용 중인 전화번호입니다.';
} else {
phoneErrorAlert.value = false;
phoneError.value = '';
}
}; };
const Mbtis = async () => { // , mbti,
const response = await $api.get('/user/mbti'); const { colorList, mbtiList, pwhintList } = commonApi({
mbtiList.value = response.data.data.map(item => ({ loadColor: true,
label: item.CMNCODNAM, colorType: 'YON',
value: item.CMNCODVAL loadMbti: true,
})); loadPwhint: true,
};
onMounted(() => {
Colors();
Mbtis();
}); });
// //
const handleAddressUpdate = (addressData) => { const handleAddressUpdate = addressData => {
address.value = addressData.address; address.value = addressData.address;
detailAddress.value = addressData.detailAddress; detailAddress.value = addressData.detailAddress;
postcode.value = addressData.postcode; // postcode.value = addressData.postcode; //
}; };
// //
const checkPw = async () => { const checkColorDuplicate = async () => {
if (password.value !== passwordcheck.value) { const response = await $api.get(`/user/checkColor?memberCol=${color.value}`);
if (response.data.data) {
colorErrorAlert.value = true;
colorError.value = '이미 사용 중인 색상입니다.';
} else {
colorErrorAlert.value = false;
colorError.value = '';
}
};
const handleColorUpdate = async newColor => {
color.value = newColor;
colorError.value = '';
colorErrorAlert.value = false;
await checkColorDuplicate();
}
const onlyNumber = (event) => {
//
if (!/^[0-9]$/.test(event.key)) {
event.preventDefault();
}
};
const inputEvent = (e) => {
const newValue = e.target.value.replace(/\D/g, ''); //
e.target.value = newValue; //
phone.value = newValue; // Vue
};
watch(id, (newValue) => {
if (newValue && newValue.length >= 4) {
idError.value = '';
idErrorAlert.value = false;
} else if (newValue && newValue.length < 4) {
idError.value = '아이디는 4자리 이상이어야 합니다.';
idErrorAlert.value = true;
}
});
watch(password, (newValue) => {
if (newValue && newValue.length >= 4) {
passwordErrorAlert.value = false;
passwordError.value = '';
} else if (newValue && newValue.length < 4) {
passwordErrorAlert.value = true;
passwordError.value = '비밀번호는 4자리 이상이어야 합니다.';
}
});
//
watch([password, passwordcheck], ([newPassword, newPasswordCheck]) => {
if (newPassword && newPasswordCheck) {
if (newPassword !== newPasswordCheck) {
passwordcheckError.value = '비밀번호가 일치하지 않습니다.'; passwordcheckError.value = '비밀번호가 일치하지 않습니다.';
passwordcheckErrorAlert.value = true; passwordcheckErrorAlert.value = true;
} else { } else {
passwordcheckError.value = ''; passwordcheckError.value = '';
passwordcheckErrorAlert.value = false; passwordcheckErrorAlert.value = false;
} }
}; }
});
// //
const handleSubmit = async () => { const handleSubmit = async () => {
await checkColorDuplicate();
idAlert.value = id.value.trim() === ''; idAlert.value = id.value.trim() === '';
passwordAlert.value = password.value.trim() === ''; passwordAlert.value = password.value.trim() === '';
@ -304,6 +388,32 @@
addressAlert.value = address.value.trim() === ''; addressAlert.value = address.value.trim() === '';
phoneAlert.value = phone.value.trim() === ''; phoneAlert.value = phone.value.trim() === '';
if (!colorList.value || colorList.value.length === 0) {
colorAlert.value = true;
}
//
if (id.value && id.value.length < 4) {
idErrorAlert.value = true;
idError.value = '아이디는 4자리 이상이어야 합니다.';
}
//
if (password.value && password.value.length < 4) {
passwordErrorAlert.value = true;
passwordError.value = '비밀번호는 4자리 이상이어야 합니다.';
} else {
passwordError.value = '';
}
const phoneRegex = /^010\d{8}$/;
const isFormatValid = phoneRegex.test(phone.value);
if (!/^\d+$/.test(phone.value) || !isFormatValid) {
phoneAlert.value = true;
} else {
phoneAlert.value = false;
}
// //
if (!profile.value) { if (!profile.value) {
profilerr.value = '프로필 이미지를 선택해주세요.'; profilerr.value = '프로필 이미지를 선택해주세요.';
@ -313,19 +423,35 @@
profilAlert.value = false; profilAlert.value = false;
} }
if (profilAlert.value || idAlert.value || idErrorAlert.value || passwordAlert.value || passwordcheckAlert.value ||
passwordcheckErrorAlert.value || pwhintResAlert.value || nameAlert.value || birthAlert.value || addressAlert.value || phoneAlert.value) { if (
profilAlert.value ||
idAlert.value ||
idErrorAlert.value ||
passwordAlert.value ||
passwordErrorAlert.value ||
passwordcheckAlert.value ||
passwordcheckErrorAlert.value ||
pwhintResAlert.value ||
nameAlert.value ||
birthAlert.value ||
addressAlert.value ||
phoneAlert.value ||
phoneErrorAlert.value ||
colorAlert.value ||
colorErrorAlert.value
) {
return; return;
} }
const formData = new FormData(); const formData = new FormData();
formData.append('memberIds', id.value); formData.append('memberIds', id.value.trim());
formData.append('memberPwd', password.value); formData.append('memberPwd', password.value.trim());
formData.append('memberPwh', pwhintList[pwhint.value]); formData.append('memberPwh', pwhint.value);
formData.append('memberPwr', pwhintRes.value); formData.append('memberPwr', pwhintRes.value.trim());
formData.append('memberNam', name.value); formData.append('memberNam', name.value.trim());
formData.append('memberArr', address.value); formData.append('memberArr', address.value);
formData.append('memberDtl', detailAddress.value); formData.append('memberDtl', detailAddress.value.trim());
formData.append('memberZip', postcode.value); formData.append('memberZip', postcode.value);
formData.append('memberBth', birth.value); formData.append('memberBth', birth.value);
formData.append('memberTel', phone.value); formData.append('memberTel', phone.value);
@ -341,5 +467,3 @@
} }
}; };
</script> </script>
<style></style>

View File

@ -1,70 +1,260 @@
<template> <template>
<!-- 컴포넌트 사용 ex) <ul v-if="displayedUserList && displayedUserList.length > 0" class="list-unstyled users-list d-flex align-items-center gap-1 flex-wrap">
<UserList @user-list-update="handleUserListUpdate" />
-->
<ul class="list-unstyled users-list d-flex align-items-center">
<li <li
v-for="(user, index) in userList" v-for="(user, index) in displayedUserList"
:key="index" :key="index"
class="avatar pull-up " class="avatar pull-up "
:class="{ disabled: user.disabled }" :class="{ 'opacity-100': isUserDisabled(user) }"
@click="toggleDisable(index)" @click.stop="showOnlyActive ? null : toggleDisable(index)"
:style="showOnlyActive ? 'cursor: default' : ''"
data-bs-toggle="tooltip" data-bs-toggle="tooltip"
data-popup="tooltip-custom" data-popup="tooltip-custom"
data-bs-placement="top" data-bs-placement="top"
:aria-label="user.MEMBERSEQ" :aria-label="user.MEMBERSEQ"
:data-bs-original-title="user.MEMBERSEQ" :data-bs-original-title="getTooltipTitle(user)"
> >
<img <img
class="rounded-circle" class="user-avatar border border-3 rounded-circle object-fit-cover"
:src="`http://localhost:10325/upload/img/profile/${user.MEMBERPRF}`" :class="{ 'grayscaleImg': isUserDisabled(user) }"
alt="profile" :src="`${baseUrl}upload/img/profile/${user.MEMBERPRF}`"
:style="`border-color: ${user.usercolor} !important;`"
@error="$event.target.src = '/img/icons/icon.png'"
alt="user"
/> />
</li> </li>
</ul> </ul>
<span v-else >-</span>
</template> </template>
<script setup> <script setup>
import { onMounted, ref } from 'vue'; import { onMounted, ref, nextTick, computed, watch } from 'vue';
import { useUserStore } from '@s/userList'; import { useUserStore } from '@s/userList';
import $api from '@api';
import { useToastStore } from "@s/toastStore";
const emit = defineEmits(); const emit = defineEmits(['user-list-update']);
const userStore = useUserStore(); const userStore = useUserStore();
const userList = ref([]); const userList = ref([]);
const baseUrl = $api.defaults.baseURL.replace(/api\/$/, '');
const userProjectPeriods = ref([]);
const toastStore = useToastStore();
const props = defineProps({
projctSeq: {
type: Number,
required: false,
},
showOnlyActive: {
type: Boolean,
default: false
},
role: {
type:String,
required:false
}
});
//
const originalDisabledUsers = ref([]);
const resetSelection = async () => {
//
if (props.projctSeq) {
//
userList.value = userList.value.map(user => ({
...user,
PROJCTYON: '1'
}));
//
await fetchProjectParticipation();
} else {
//
userList.value = userList.value.map(user => ({
...user,
disabled: false //
}));
}
emitUserListUpdate();
};
// computed
const sortedUserList = computed(() => {
return [...userList.value].sort((a, b) => {
const aDisabled = isUserDisabled(a);
const bDisabled = isUserDisabled(b);
//
if (!aDisabled && bDisabled) return -1;
if (aDisabled && !bDisabled) return 1;
return 0;
});
});
// showOnlyActive true ,
const displayedUserList = computed(() => {
if (props.showOnlyActive) {
return sortedUserList.value.filter(user => !isUserDisabled(user));
}
return sortedUserList.value;
});
//
const fetchProjectParticipation = async () => {
if (props.projctSeq) {
const response = await $api.get(`project/members/${props.projctSeq}`);
if (response.status === 200) {
const projectMembers = response.data.data;
userList.value = userList.value.map(user => ({
...user,
PROJCTYON: projectMembers.find(pm => pm.MEMBERSEQ === user.MEMBERSEQ)?.PROJCTYON ?? '1'
}));
//
originalDisabledUsers.value = userList.value
.filter(user => user.PROJCTYON === '0')
.map(user => user.MEMBERSEQ);
emitUserListUpdate();
}
}
};
//
const fetchUserProjectPeriods = async () => {
if (props.projctSeq) {
try {
const response = await $api.get(`project/period/${props.projctSeq}`);
if (response.status === 200) {
userProjectPeriods.value = response.data.data;
}
} catch (error) {
console.error('프로젝트 참여 기간 조회 실패:', error);
}
}
};
// projctSeq
watch(() => props.projctSeq, async (newVal, oldVal) => {
if (newVal !== oldVal) {
await fetchProjectParticipation();
await fetchUserProjectPeriods();
}
}, { immediate: true });
// //
onMounted(async () => { onMounted(async () => {
await userStore.fetchUserList(); await userStore.fetchUserList();
userList.value = userStore.userList; userList.value = userStore.userList;
if (props.projctSeq) {
await fetchProjectParticipation();
await fetchUserProjectPeriods();
} else {
// projctSeq , emit
emitUserListUpdate();
}
nextTick(() => {
initTooltips();
});
}); });
// / //
const initTooltips = () => {
const tooltips = document.querySelectorAll('[data-bs-toggle="tooltip"]');
tooltips.forEach((tooltip) => {
if (tooltip._tooltip) {
tooltip._tooltip.dispose();
}
new bootstrap.Tooltip(tooltip);
});
};
//
const isUserDisabled = (user) => {
return props.projctSeq ? user.PROJCTYON === '0' : user.disabled;
};
// ( API )
const toggleDisable = (index) => { const toggleDisable = (index) => {
const user = userList.value[index]; if (props.showOnlyActive) return;
const user = displayedUserList.value[index];
//
if (props.role === 'vote') {
if (user.MEMBERSEQ === userStore.userInfo.id) {
toastStore.onToast('본인은 비활성화할 수 없습니다.', 'e');
return;
}
}
if (user) { if (user) {
//
if (props.projctSeq) {
user.PROJCTYON = user.PROJCTYON === '1' ? '0' : '1';
} else {
user.disabled = !user.disabled; user.disabled = !user.disabled;
}
emitUserListUpdate(); emitUserListUpdate();
} }
}; };
// emit // emit
const emitUserListUpdate = () => { const emitUserListUpdate = () => {
const activeUsers = userList.value.filter(user => !user.disabled); const activeUsers = userList.value.filter(user => !isUserDisabled(user));
const disabledUsers = userList.value.filter(user => user.disabled); const disabledUsers = userList.value.filter(user => isUserDisabled(user));
emit('user-list-update', { activeUsers, disabledUsers }); emit('user-list-update', { activeUsers, disabledUsers });
}; };
</script>
<style scoped> //
/* disabled 클래스를 적용할 때 사용자의 이미지를 흐리게 */ const formatDate = (dateString) => {
.avatar.disabled { if (!dateString) return '';
opacity: 0.5; /* 흐리게 만들기 */ const date = new Date(dateString);
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
};
//
const getTooltipTitle = (user) => {
const userName = user.MEMBERSEQ === userStore.userInfo.id ? '나' : user.MEMBERNAM;
if (props.projctSeq) {
const periodInfo = userProjectPeriods.value.find(p => p.MEMBERSEQ === user.MEMBERSEQ);
if (periodInfo) {
return `${formatDate(periodInfo.userStartDate)} ~ ${formatDate(periodInfo.userEndDate)}`;
}
} }
/* 비활성화된 상태에서 이미지를 회색으로 변환 */ return userName;
.avatar.disabled img { };
filter: grayscale(100%);
const hasUserChanges = () => {
if (!props.projctSeq) return false;
const currentDisabledUserIds = userList.value
.filter(user => user.PROJCTYON === '0')
.map(user => user.MEMBERSEQ);
//
if (currentDisabledUserIds.length !== originalDisabledUsers.value.length) {
return true;
} }
</style>
// ID
return currentDisabledUserIds.some(id => !originalDisabledUsers.value.includes(id)) ||
originalDisabledUsers.value.some(id => !currentDisabledUserIds.includes(id));
};
// expose
defineExpose({
resetSelection,
fetchProjectParticipation,
hasUserChanges
});
</script>

Some files were not shown because too many files have changed in this diff Show More