From bd34b409915fff779b7213ce9142152a262e63c8 Mon Sep 17 00:00:00 2001 From: Minseo-Jo Date: Tue, 21 Oct 2025 10:12:18 +0900 Subject: [PATCH] =?UTF-8?q?=ED=94=84=EB=A1=9C=EC=A0=9D=ED=8A=B8=20?= =?UTF-8?q?=EA=B5=AC=EC=A1=B0=20=EC=A0=95=EB=A6=AC=20=EB=B0=8F=20=ED=94=84?= =?UTF-8?q?=EB=A1=9C=ED=86=A0=ED=83=80=EC=9E=85=20=EC=97=85=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - design-last, design-v1 디렉토리 정리 - UI/UX 프로토타입 개선 및 통합 - 스타일 가이드 및 테스트 결과 업데이트 - 유저스토리 목록 추가 - 불필요한 문서 제거 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .claude/settings.local.json | 5 +- design-last/uiux/prototype/01-로그인.html | 541 ----- design-last/uiux/prototype/02-대시보드.html | 709 ------ design-last/uiux/prototype/03-회의예약.html | 612 ----- design-last/uiux/prototype/04-템플릿선택.html | 1006 -------- design-last/uiux/prototype/05-회의진행.html | 537 ----- design-last/uiux/prototype/06-검증완료.html | 517 ---- design-last/uiux/prototype/07-회의종료.html | 472 ---- design-last/uiux/prototype/common.css | 1293 ---------- design-last/uiux/prototype/common.js | 1100 --------- design-last/uiux/uiux.md | 1558 ------------- .../uiux_다람지/prototype/01-로그인.html | 170 -- .../uiux_다람지/prototype/02-대시보드.html | 225 -- .../uiux_다람지/prototype/03-회의예약.html | 350 --- .../uiux_다람지/prototype/04-템플릿선택.html | 234 -- .../uiux_다람지/prototype/05-회의진행.html | 434 ---- .../uiux_다람지/prototype/06-검증완료.html | 219 -- .../uiux_다람지/prototype/07-회의종료.html | 211 -- .../uiux_다람지/prototype/08-회의록공유.html | 253 -- .../uiux_다람지/prototype/09-Todo관리.html | 280 --- design-last/uiux_다람지/prototype/common.css | 1006 -------- design-last/uiux_다람지/prototype/common.js | 990 -------- design-last/uiux_다람지/style-guide.md | 1767 -------------- design-last/userstory.md | 768 ------ design-v1/uiux/prototype/01-로그인.html | 307 --- design-v1/uiux/prototype/02-대시보드.html | 539 ----- design-v1/uiux/prototype/03-회의예약.html | 699 ------ design-v1/uiux/prototype/04-템플릿선택.html | 537 ----- design-v1/uiux/prototype/05-회의진행.html | 858 ------- design-v1/uiux/prototype/06-검증완료.html | 499 ---- design-v1/uiux/prototype/07-회의종료.html | 573 ----- design-v1/uiux/prototype/08-회의록공유.html | 610 ----- design-v1/uiux/prototype/09-Todo관리.html | 749 ------ design-v1/uiux/prototype/common.css | 596 ----- design-v1/uiux/prototype/common.js | 408 ---- design-v1/uiux/style-guide.md | 1184 ---------- design-v1/uiux/uiux.md | 1449 ------------ design-v1/userstory.md | 674 ------ design/backend/api/meeting-dashboard-api.md | 715 ------ design/style-guide.md | 1184 ---------- design/uiux/prototype/01-로그인.html | 740 +++--- design/uiux/prototype/02-대시보드.html | 1082 ++++----- design/uiux/prototype/03-회의예약.html | 663 +++++- design/uiux/prototype/04-템플릿선택.html | 1079 +++++++-- design/uiux/prototype/05-회의진행.html | 1102 ++++----- design/uiux/prototype/06-검증완료.html | 664 ++++-- design/uiux/prototype/07-회의종료.html | 536 ++++- design/uiux/prototype/08-최종확정.html | 303 --- .../uiux/prototype/08-회의록공유.html | 0 .../uiux/prototype/09-Todo관리.html | 0 design/uiux/prototype/09-회의록공유.html | 316 --- design/uiux/prototype/10-Todo관리.html | 466 ---- design/uiux/prototype/11-회의록대시보드.html | 1021 -------- .../uiux/prototype/TEST_RESULTS.md | 0 design/uiux/prototype/common.css | 1719 +++++++++----- design/uiux/prototype/common.js | 1556 +++++++++---- design/uiux/prototype/test-report.md | 212 -- {design-last => design}/uiux/style-guide.md | 0 design/uiux/uiux.md | 2074 ++++++++++++----- design/uiux_다람지/prototype/01-로그인.html | 219 +- design/uiux_다람지/prototype/02-대시보드.html | 361 +-- design/uiux_다람지/prototype/03-회의예약.html | 528 +++-- .../uiux_다람지/prototype/04-템플릿선택.html | 315 ++- design/uiux_다람지/prototype/05-회의진행.html | 543 +++-- design/uiux_다람지/prototype/06-검증완료.html | 375 ++- design/uiux_다람지/prototype/07-회의종료.html | 372 ++- .../uiux_다람지/prototype/08-회의록공유.html | 488 ++-- design/uiux_다람지/prototype/09-Todo관리.html | 601 ++--- .../prototype/10-회의록상세조회.html | 0 .../uiux_다람지/prototype/11-회의록수정.html | 0 .../uiux_다람지/prototype/TEST_RESULTS.md | 0 design/uiux_다람지/prototype/common.css | 1289 ++++++---- design/uiux_다람지/prototype/common.js | 1354 +++++++---- design/uiux_다람지/style-guide.md | 1891 ++++++++++++--- {design-last => design}/uiux_다람지/uiux.md | 0 design/userstory-table.md | 300 --- design/userstory.md | 512 +--- {design-last => design}/userstory_list.md | 0 design/구현방안-맥락기반용어설명.md | 976 -------- 79 files changed, 12954 insertions(+), 37541 deletions(-) delete mode 100644 design-last/uiux/prototype/01-로그인.html delete mode 100644 design-last/uiux/prototype/02-대시보드.html delete mode 100644 design-last/uiux/prototype/03-회의예약.html delete mode 100644 design-last/uiux/prototype/04-템플릿선택.html delete mode 100644 design-last/uiux/prototype/05-회의진행.html delete mode 100644 design-last/uiux/prototype/06-검증완료.html delete mode 100644 design-last/uiux/prototype/07-회의종료.html delete mode 100644 design-last/uiux/prototype/common.css delete mode 100644 design-last/uiux/prototype/common.js delete mode 100644 design-last/uiux/uiux.md delete mode 100644 design-last/uiux_다람지/prototype/01-로그인.html delete mode 100644 design-last/uiux_다람지/prototype/02-대시보드.html delete mode 100644 design-last/uiux_다람지/prototype/03-회의예약.html delete mode 100644 design-last/uiux_다람지/prototype/04-템플릿선택.html delete mode 100644 design-last/uiux_다람지/prototype/05-회의진행.html delete mode 100644 design-last/uiux_다람지/prototype/06-검증완료.html delete mode 100644 design-last/uiux_다람지/prototype/07-회의종료.html delete mode 100644 design-last/uiux_다람지/prototype/08-회의록공유.html delete mode 100644 design-last/uiux_다람지/prototype/09-Todo관리.html delete mode 100644 design-last/uiux_다람지/prototype/common.css delete mode 100644 design-last/uiux_다람지/prototype/common.js delete mode 100644 design-last/uiux_다람지/style-guide.md delete mode 100644 design-last/userstory.md delete mode 100644 design-v1/uiux/prototype/01-로그인.html delete mode 100644 design-v1/uiux/prototype/02-대시보드.html delete mode 100644 design-v1/uiux/prototype/03-회의예약.html delete mode 100644 design-v1/uiux/prototype/04-템플릿선택.html delete mode 100644 design-v1/uiux/prototype/05-회의진행.html delete mode 100644 design-v1/uiux/prototype/06-검증완료.html delete mode 100644 design-v1/uiux/prototype/07-회의종료.html delete mode 100644 design-v1/uiux/prototype/08-회의록공유.html delete mode 100644 design-v1/uiux/prototype/09-Todo관리.html delete mode 100644 design-v1/uiux/prototype/common.css delete mode 100644 design-v1/uiux/prototype/common.js delete mode 100644 design-v1/uiux/style-guide.md delete mode 100644 design-v1/uiux/uiux.md delete mode 100644 design-v1/userstory.md delete mode 100644 design/backend/api/meeting-dashboard-api.md delete mode 100644 design/style-guide.md delete mode 100644 design/uiux/prototype/08-최종확정.html rename {design-last => design}/uiux/prototype/08-회의록공유.html (100%) rename {design-last => design}/uiux/prototype/09-Todo관리.html (100%) delete mode 100644 design/uiux/prototype/09-회의록공유.html delete mode 100644 design/uiux/prototype/10-Todo관리.html delete mode 100644 design/uiux/prototype/11-회의록대시보드.html rename {design-last => design}/uiux/prototype/TEST_RESULTS.md (100%) delete mode 100644 design/uiux/prototype/test-report.md rename {design-last => design}/uiux/style-guide.md (100%) rename {design-last => design}/uiux_다람지/prototype/10-회의록상세조회.html (100%) rename {design-last => design}/uiux_다람지/prototype/11-회의록수정.html (100%) rename {design-last => design}/uiux_다람지/prototype/TEST_RESULTS.md (100%) rename {design-last => design}/uiux_다람지/uiux.md (100%) delete mode 100644 design/userstory-table.md rename {design-last => design}/userstory_list.md (100%) delete mode 100644 design/구현방안-맥락기반용어설명.md diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 86c284e..b2c7054 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -25,7 +25,10 @@ "Bash(git add design-last/uiux/)", "Bash(git commit -m \"$(cat <<''EOF''\nUI/UX 설계서 작성 완료\n\n- Mobile First 설계 원칙에 따라 UI/UX 설계서 작성\n- 11개 주요 화면 설계 (로그인, 대시보드, 회의예약, 템플릿선택, 회의진행, 검증완료, 회의종료, 회의록공유, Todo관리, 회의록상세조회, 회의록수정)\n- 화면별 상세 설계 (개요, 기능, UI 구성, 인터랙션, 데이터 요구사항, 에러 처리)\n- 화면 간 사용자 플로우 및 네비게이션 전략 정의\n- 반응형 설계 전략 (Mobile/Tablet/Desktop 브레이크포인트)\n- WCAG 2.1 Level AA 접근성 보장 방안\n- 성능 최적화 방안 (코드 스플리팅, 캐싱, WebSocket 최적화)\n- 유저스토리와 1:1 매칭 확인\n\n🤖 Generated with [Claude Code](https://claude.com/claude-code)\n\nCo-Authored-By: Claude \nEOF\n)\")", "Bash(git add \"design-last/uiux_다람지/\")", - "Bash(git commit -m \"$(cat <<''EOF''\n프로토타입 개발 완료 (다람지팀)\n\n- 스타일 가이드 작성 (style-guide.md)\n - 14개 섹션으로 구성된 완전한 디자인 시스템\n - Mobile First 철학 및 접근성 기준 정의\n \n- 공통 리소스 개발\n - common.css: 1,007줄 완전한 반응형 스타일시트\n - common.js: 400+줄 유틸리티 라이브러리\n \n- 11개 프로토타입 화면 개발\n - 01-로그인: 사용자 인증\n - 02-대시보드: 메인 대시보드\n - 03-회의예약: 회의 생성 폼\n - 04-템플릿선택: 회의록 템플릿 선택\n - 05-회의진행: 실시간 회의 진행\n - 06-검증완료: 섹션별 검증\n - 07-회의종료: 회의 통계\n - 08-회의록공유: 공유 설정\n - 09-Todo관리: Todo 목록 및 진행 관리\n - 10-회의록상세조회: 회의록 상세 보기\n - 11-회의록수정: 지난 회의록 수정\n \n- 주요 특징\n - Mobile First 반응형 디자인\n - WCAG 2.1 Level AA 접근성 준수\n - 실제 동작하는 인터랙션 구현\n - 일관된 예제 데이터 활용\n - 완전한 사용자 플로우 구현\n\n🤖 Generated with [Claude Code](https://claude.com/claude-code)\n\nCo-Authored-By: Claude \nEOF\n)\")" + "Bash(git commit -m \"$(cat <<''EOF''\n프로토타입 개발 완료 (다람지팀)\n\n- 스타일 가이드 작성 (style-guide.md)\n - 14개 섹션으로 구성된 완전한 디자인 시스템\n - Mobile First 철학 및 접근성 기준 정의\n \n- 공통 리소스 개발\n - common.css: 1,007줄 완전한 반응형 스타일시트\n - common.js: 400+줄 유틸리티 라이브러리\n \n- 11개 프로토타입 화면 개발\n - 01-로그인: 사용자 인증\n - 02-대시보드: 메인 대시보드\n - 03-회의예약: 회의 생성 폼\n - 04-템플릿선택: 회의록 템플릿 선택\n - 05-회의진행: 실시간 회의 진행\n - 06-검증완료: 섹션별 검증\n - 07-회의종료: 회의 통계\n - 08-회의록공유: 공유 설정\n - 09-Todo관리: Todo 목록 및 진행 관리\n - 10-회의록상세조회: 회의록 상세 보기\n - 11-회의록수정: 지난 회의록 수정\n \n- 주요 특징\n - Mobile First 반응형 디자인\n - WCAG 2.1 Level AA 접근성 준수\n - 실제 동작하는 인터랙션 구현\n - 일관된 예제 데이터 활용\n - 완전한 사용자 플로우 구현\n\n🤖 Generated with [Claude Code](https://claude.com/claude-code)\n\nCo-Authored-By: Claude \nEOF\n)\")", + "Bash(git add \"design-last/uiux_다람지/prototype/01-로그인.html\")", + "Bash(git commit -m \"로그인 페이지 버그 수정\n\n- CSS keyframes를 script 태그에서 style 태그로 이동\n- FormValidator 검증을 간단한 검증으로 변경하여 안정성 향상\n- 로그인 후 대시보드 이동 기능 정상화\n\n🤖 Generated with [Claude Code](https://claude.com/claude-code)\n\nCo-Authored-By: Claude \")", + "Bash(git commit -m \"프로젝트 구조 정리 및 프로토타입 업데이트\n\n- design-last, design-v1 디렉토리 정리\n- UI/UX 프로토타입 개선 및 통합\n- 스타일 가이드 및 테스트 결과 업데이트\n- 유저스토리 목록 추가\n- 불필요한 문서 제거\n\n🤖 Generated with [Claude Code](https://claude.com/claude-code)\n\nCo-Authored-By: Claude \")" ], "deny": [], "ask": [] diff --git a/design-last/uiux/prototype/01-로그인.html b/design-last/uiux/prototype/01-로그인.html deleted file mode 100644 index 3cf8fcc..0000000 --- a/design-last/uiux/prototype/01-로그인.html +++ /dev/null @@ -1,541 +0,0 @@ - - - - - - - 로그인 - 회의록 작성 및 공유 개선 서비스 - - - - - - - - - - - - 본문으로 바로가기 - - -
-
-
-

로그인 중입니다...

-
-
- - -
- -
- - - - - - diff --git a/design-last/uiux/prototype/02-대시보드.html b/design-last/uiux/prototype/02-대시보드.html deleted file mode 100644 index 7c6ce1c..0000000 --- a/design-last/uiux/prototype/02-대시보드.html +++ /dev/null @@ -1,709 +0,0 @@ - - - - - - - 대시보드 - 회의록 작성 서비스 - - - - - - - - - - - -
-
-

대시보드

-
-
- - -
-
- - -
- - -
- - -
-
- - -
-
- 🔍 - -
-
- - -
-
-

내 회의록

- 0건 -
-
- -
-
- - - - - - - - - - - - diff --git a/design-last/uiux/prototype/03-회의예약.html b/design-last/uiux/prototype/03-회의예약.html deleted file mode 100644 index fd67fe6..0000000 --- a/design-last/uiux/prototype/03-회의예약.html +++ /dev/null @@ -1,612 +0,0 @@ - - - - - - - 회의 예약 - 회의록 작성 서비스 - - - - - - - - - - -
- -
-
- -

회의 예약

-
- -
- - -
-
- -
-

기본 정보

- - -
- - - -

최대 100자까지 입력 가능합니다

-
- - -
- -
-
- - -
-
- - -
-
-
- - -
- - -

최대 200자까지 입력 가능합니다

-
-
- - -
-

참석자

- -
- -
- -
-
- - -
- -

최소 1명 이상의 참석자를 추가해주세요

-
-
- - -
-

알림 설정

- -
-
-
- -
-
-
-
-
- - -
- -
-
- - - - - - diff --git a/design-last/uiux/prototype/04-템플릿선택.html b/design-last/uiux/prototype/04-템플릿선택.html deleted file mode 100644 index 2efdbeb..0000000 --- a/design-last/uiux/prototype/04-템플릿선택.html +++ /dev/null @@ -1,1006 +0,0 @@ - - - - - - - 템플릿 선택 - 회의록 작성 및 공유 개선 서비스 - - - - - - - - - - - -
- -
- -

템플릿 선택

-

회의 목적에 맞는 템플릿을 선택하고 필요한 경우 커스터마이징하세요

-
- - -
- - - - - - - - - - - -
-
- - -
- -
- - - - - - - - - - - - diff --git a/design-last/uiux/prototype/05-회의진행.html b/design-last/uiux/prototype/05-회의진행.html deleted file mode 100644 index 8b13c52..0000000 --- a/design-last/uiux/prototype/05-회의진행.html +++ /dev/null @@ -1,537 +0,0 @@ - - - - - - - 회의 진행 - 회의록 작성 서비스 - - - - - 본문으로 건너뛰기 - - -
-
- -

프로젝트 킥오프

- -
-
- - -
- - -
-
-
-
23:45
- -
-
- - -
-

👥 참석자 (3/5명)

-
-
- 👨‍💼 - 김민준 -
-
- 👩‍💻 - 박서연 -
-
- 👨‍💻 - 이준호 -
-
-
- - -
-

📝 실시간 회의록

- - -
- - -
- - -
- - -
- - -
- -
- -
-
- 김민준 - 14:23 -
-
- 우리는 Q1까지 - MVP를 완성해야 합니다. - 개발 프레임워크는 React를 사용하고, - 배포 환경은 AWS로 결정했습니다. - -
-
- - -
-
- 💡 - AI 자동 정리 -
-

- Q1(1분기)까지 MVP(최소 기능 제품) 완성을 목표로 설정했습니다. - 개발 프레임워크로 React를 선택하고, 배포 환경은 AWS를 사용하기로 결정했습니다. -

-
- - -
-
- 박서연 - 14:24 - 수정 중... -
-
- Sprint 주기는 2주로 하는 게 좋을 것 같습니다. -
-
- - -
- - -
-
-
- - -
- - -
- - -
- - -
-
- - - - -
- - - - - - - - - - - diff --git a/design-last/uiux/prototype/06-검증완료.html b/design-last/uiux/prototype/06-검증완료.html deleted file mode 100644 index c5a5fe1..0000000 --- a/design-last/uiux/prototype/06-검증완료.html +++ /dev/null @@ -1,517 +0,0 @@ - - - - - - - 회의록 검증 - 회의록 작성 서비스 - - - - - 본문으로 건너뛰기 - - -
-
- -

회의록 검증

- -
-
- - -
- - -
-
-
-

전체 진행률

- 60% (3/5) -
-
-
-
-
-

- 회의록 섹션별로 검증해주세요. 모든 섹션이 검증되면 회의를 종료할 수 있습니다. -

-
- - -
-

섹션별 검증

- - -
-
-
-

✅ 참석자

- 검증완료 -
-
- 검증자: 김민준 - - 시간: 14:35 -
-
-
-

- 김민준 (주관자)

-

- 박서연

-

- 이준호

-
- -
- - -
-
-
-

⚠️ 안건

- 검증 필요 -
-
-
-

- 프로젝트 목표 정의

-

- 일정 및 마일스톤

-
- -
- - -
-
-
-

⚠️ 논의 내용

- 검증 필요 -
-
-
-

- 우리는 Q1까지 MVP를 완성해야 합니다. 개발 프레임워크는 React를 사용하고, 배포 환경은 AWS로 결정했습니다. - Sprint 주기는 2주로 설정합니다. -

-
- -
- - -
-
-
-

✅ 결정 사항

- 검증완료 -
-
- 검증자: 박서연 - - 시간: 14:40 -
-
-
-

- 개발 프레임워크: React

-

- 배포 환경: AWS

-

- Sprint 주기: 2주

-
- -
- - -
-
-
-

✅ Todo

- 검증완료 -
-
- 검증자: 이준호 - - 시간: 14:42 -
-
-
-
- -
-
요구사항 정의
-
- @김민준 - (~ 10/25) -
-
-
-
- -
-
기술 스택 검토
-
- @박서연 - (~ 10/27) -
-
-
-
- -
- -
- - -
-
- 💡 - 안내 -
-

- 검증 미완료 섹션이 있어도 다음 단계로 진행할 수 있습니다. 나중에 수정하고 다시 확정할 수 있습니다. -

-
- -
- - - - - - - - - - - diff --git a/design-last/uiux/prototype/07-회의종료.html b/design-last/uiux/prototype/07-회의종료.html deleted file mode 100644 index fd38222..0000000 --- a/design-last/uiux/prototype/07-회의종료.html +++ /dev/null @@ -1,472 +0,0 @@ - - - - - - - 회의 종료 - 회의록 작성 서비스 - - - - - 본문으로 건너뛰기 - - -
-
- -

회의 종료

- -
-
- - -
- - -
-
🎉
-

회의가 종료되었습니다

-

- 회의록을 확인하고 최종 확정해주세요 -

-
- - -
-

📊 회의 통계

-
-
- -
-
- ⏱️ - 총 시간 -
-
45분
-
- - -
-
- 👥 - 참석자 -
-
3명
-
-
- - -
-
- 💬 - 발언 횟수 -
-
-
- 김민준 -
-
-
-
- 12회 -
-
-
- 박서연 -
-
-
-
- 8회 -
-
-
- 이준호 -
-
-
-
- 5회 -
-
-
-
- - -
-
- 🔑 - 주요 키워드 -
-
- #MVP - #React - #AWS - #Sprint - #Q1 -
-
-
-
- - -
-

✅ AI Todo 자동 추출

-
-
- 💡 - AI가 회의록에서 3개의 Todo를 자동으로 추출했습니다 -
- - -
- -
-
요구사항 정의서 작성
-
- @김민준 - 📅 ~ 10/25 - -
-
-
- - -
- -
-
기술 스택 상세 검토
-
- @박서연 - 📅 ~ 10/27 - -
-
-
- - -
- -
-
인프라 설계 문서 작성
-
- @이준호 - 📅 ~ 10/30 - -
-
-
- -
- -
-
-
- - -
-

필수 항목 확인

-
-
-
- - 회의 제목 -
-
- - 참석자 목록 -
-
- - 주요 논의 내용 -
-
- - 결정 사항 -
-
-
-
- - -
- - -
- -
- - - - - - - - - - - diff --git a/design-last/uiux/prototype/common.css b/design-last/uiux/prototype/common.css deleted file mode 100644 index 02893db..0000000 --- a/design-last/uiux/prototype/common.css +++ /dev/null @@ -1,1293 +0,0 @@ -/* - * 회의록 작성 및 공유 개선 서비스 - 공통 스타일시트 - * 버전: 1.0 - * 작성일: 2025-10-20 - * 설계 철학: Mobile First 디자인 - * 접근성 기준: WCAG 2.1 Level AA - */ - -/* ==================== CSS Variables ==================== */ -:root { - /* 컬러 - Primary (청록색) */ - --primary-50: #ECFDF5; - --primary-100: #D1FAE5; - --primary-200: #A7F3D0; - --primary-500: #00C896; - --primary-600: #00B589; - --primary-700: #00A07C; - --primary-900: #00725C; - - /* 컬러 - Gray (회색 스케일) */ - --gray-50: #F9FAFB; - --gray-100: #F3F4F6; - --gray-200: #E5E7EB; - --gray-300: #D1D5DB; - --gray-400: #9CA3AF; - --gray-500: #6B7280; - --gray-600: #4B5563; - --gray-700: #374151; - --gray-800: #1F2937; - --gray-900: #111827; - - /* 컬러 - Semantic (의미 색상) */ - --success-50: #ECFDF5; - --success-100: #D1FAE5; - --success-500: #10B981; - --success-700: #047857; - - --warning-50: #FFFBEB; - --warning-100: #FEF3C7; - --warning-500: #F59E0B; - --warning-700: #B45309; - - --error-50: #FEF2F2; - --error-100: #FEE2E2; - --error-500: #EF4444; - --error-700: #B91C1C; - - --info-50: #EFF6FF; - --info-100: #DBEAFE; - --info-500: #3B82F6; - --info-700: #1D4ED8; - - /* 배경 색상 */ - --bg-primary: #FFFFFF; - --bg-secondary: #F9FAFB; - --bg-tertiary: #F3F4F6; - --bg-dark: #111827; - - /* 텍스트 색상 */ - --text-primary: #111827; - --text-secondary: #6B7280; - --text-tertiary: #9CA3AF; - --text-disabled: #D1D5DB; - --text-inverse: #FFFFFF; - - /* 폰트 패밀리 */ - --font-primary: 'Pretendard', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif; - --font-mono: 'SF Mono', 'Monaco', 'Consolas', 'Liberation Mono', monospace; - - /* 간격 시스템 (8px 기반 그리드) */ - --space-0: 0px; - --space-1: 4px; - --space-2: 8px; - --space-3: 12px; - --space-4: 16px; - --space-5: 20px; - --space-6: 24px; - --space-8: 32px; - --space-10: 40px; - --space-12: 48px; - --space-16: 64px; - - /* Border Radius */ - --radius-none: 0px; - --radius-small: 4px; - --radius-medium: 8px; - --radius-large: 12px; - --radius-xlarge: 16px; - --radius-full: 9999px; - - /* Border Width */ - --border-thin: 1px; - --border-medium: 2px; - --border-thick: 4px; - - /* 애니메이션 지속 시간 */ - --duration-instant: 100ms; - --duration-fast: 150ms; - --duration-normal: 200ms; - --duration-slow: 300ms; - --duration-slower: 500ms; - - /* 아이콘 크기 */ - --icon-small: 16px; - --icon-medium: 20px; - --icon-large: 24px; - --icon-xlarge: 32px; -} - -/* ==================== Reset CSS ==================== */ -*, *::before, *::after { - box-sizing: border-box; - margin: 0; - padding: 0; -} - -html { - font-size: 16px; - -webkit-text-size-adjust: 100%; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} - -body { - font-family: var(--font-primary); - font-size: 0.875rem; /* 14px */ - line-height: 1.5; - color: var(--text-secondary); - background-color: var(--bg-secondary); - min-height: 100vh; -} - -img, picture, video, canvas, svg { - display: block; - max-width: 100%; -} - -input, button, textarea, select { - font: inherit; -} - -p, h1, h2, h3, h4, h5, h6 { - overflow-wrap: break-word; -} - -ul, ol { - list-style: none; -} - -a { - color: inherit; - text-decoration: none; -} - -button { - background: none; - border: none; - cursor: pointer; -} - -/* ==================== Typography ==================== */ -/* Mobile First */ -h1, .h1 { - font-size: 1.5rem; /* 24px */ - font-weight: 700; - line-height: 1.25; - letter-spacing: -0.01em; - color: var(--text-primary); -} - -h2, .h2 { - font-size: 1.25rem; /* 20px */ - font-weight: 600; - line-height: 1.25; - color: var(--text-primary); -} - -h3, .h3 { - font-size: 1.125rem; /* 18px */ - font-weight: 600; - line-height: 1.25; - color: var(--text-primary); -} - -h4, .h4 { - font-size: 1rem; /* 16px */ - font-weight: 600; - line-height: 1.25; - color: var(--text-primary); -} - -.text-body { - font-size: 0.875rem; /* 14px */ - font-weight: 400; - line-height: 1.5; - color: var(--text-secondary); -} - -.text-caption { - font-size: 0.75rem; /* 12px */ - font-weight: 400; - line-height: 1.5; - letter-spacing: 0.02em; - color: var(--text-tertiary); -} - -.text-small { - font-size: 0.6875rem; /* 11px */ - font-weight: 400; - line-height: 1.5; - color: var(--text-tertiary); -} - -/* Tablet */ -@media (min-width: 768px) { - h1, .h1 { font-size: 1.75rem; /* 28px */ } - h2, .h2 { font-size: 1.375rem; /* 22px */ } - h3, .h3 { font-size: 1.25rem; /* 20px */ } - h4, .h4 { font-size: 1.125rem; /* 18px */ } - .text-body { font-size: 1rem; /* 16px */ } - .text-caption { font-size: 0.875rem; /* 14px */ } - .text-small { font-size: 0.75rem; /* 12px */ } -} - -/* Desktop */ -@media (min-width: 1024px) { - h1, .h1 { font-size: 2rem; /* 32px */ } - h2, .h2 { font-size: 1.5rem; /* 24px */ } -} - -/* Display (Desktop only) */ -@media (min-width: 1024px) { - .display { - font-size: 2.5rem; /* 40px */ - font-weight: 700; - line-height: 1.25; - letter-spacing: -0.01em; - color: var(--text-primary); - } -} - -@media (min-width: 1440px) { - .display { - font-size: 3rem; /* 48px */ - } -} - -/* ==================== Button Components ==================== */ -/* Base Button */ -.button { - display: inline-flex; - align-items: center; - justify-content: center; - gap: var(--space-2); - font-weight: 600; - border-radius: var(--radius-small); - transition: all var(--duration-instant) ease-in-out; - cursor: pointer; - min-height: 44px; - min-width: 44px; -} - -.button:disabled { - cursor: not-allowed; - opacity: 0.6; -} - -/* Primary Button */ -.button-primary { - background-color: var(--primary-500); - color: var(--text-inverse); - border: none; - font-size: 0.875rem; /* 14px */ - padding: var(--space-3) var(--space-4); /* 12px 16px */ -} - -.button-primary:hover:not(:disabled) { - background-color: var(--primary-600); -} - -.button-primary:active:not(:disabled) { - background-color: var(--primary-700); -} - -.button-primary:disabled { - background-color: var(--gray-300); - color: var(--text-disabled); -} - -/* Secondary Button */ -.button-secondary { - background-color: var(--gray-50); - color: var(--text-secondary); - border: var(--border-thin) solid var(--gray-200); - font-size: 0.875rem; /* 14px */ - padding: var(--space-3) var(--space-4); /* 12px 16px */ -} - -.button-secondary:hover:not(:disabled) { - background-color: var(--gray-100); - border-color: var(--gray-300); -} - -.button-secondary:active:not(:disabled) { - background-color: var(--gray-200); -} - -/* Outline Button */ -.button-outline { - background-color: transparent; - color: var(--primary-500); - border: var(--border-thin) solid var(--primary-500); - font-size: 0.875rem; /* 14px */ - padding: var(--space-3) var(--space-4); /* 12px 16px */ -} - -.button-outline:hover:not(:disabled) { - background-color: var(--primary-50); -} - -.button-outline:active:not(:disabled) { - background-color: var(--primary-100); -} - -/* Ghost Button */ -.button-ghost { - background-color: transparent; - color: var(--text-secondary); - border: none; - font-size: 0.875rem; /* 14px */ - padding: var(--space-3) var(--space-4); /* 12px 16px */ -} - -.button-ghost:hover:not(:disabled) { - background-color: var(--gray-50); -} - -.button-ghost:active:not(:disabled) { - background-color: var(--gray-100); -} - -/* Button Sizes */ -.button-small { - height: 32px; - padding: var(--space-2) var(--space-3); /* 8px 12px */ - font-size: 0.75rem; /* 12px */ -} - -.button-medium { - height: 40px; - padding: var(--space-3) var(--space-4); /* 12px 16px */ - font-size: 0.875rem; /* 14px */ -} - -.button-large { - height: 48px; - padding: var(--space-4) var(--space-6); /* 16px 24px */ - font-size: 1rem; /* 16px */ -} - -/* Icon Button */ -.button-icon { - width: 44px; - height: 44px; - padding: var(--space-3); - display: inline-flex; - align-items: center; - justify-content: center; -} - -/* ==================== Input Field ==================== */ -.input-group { - display: flex; - flex-direction: column; - gap: var(--space-2); -} - -.input-label { - display: block; - font-size: 0.875rem; /* 14px */ - font-weight: 500; - color: var(--gray-700); -} - -.input-label.required::after { - content: ' *'; - color: var(--error-500); -} - -.input-field { - width: 100%; - background-color: var(--bg-primary); - color: var(--text-primary); - border: var(--border-thin) solid var(--gray-200); - border-radius: var(--radius-small); - font-size: 0.875rem; /* 14px */ - height: 40px; - padding: 0 var(--space-4); - transition: border-color var(--duration-fast) ease-in-out; -} - -.input-field:focus { - outline: none; - border-color: var(--primary-500); - box-shadow: 0 0 0 3px rgba(0, 200, 150, 0.1); -} - -.input-field:disabled { - background-color: var(--bg-secondary); - color: var(--text-disabled); - cursor: not-allowed; -} - -.input-field.error { - border-color: var(--error-500); -} - -.input-field::placeholder { - color: var(--text-tertiary); -} - -.input-error-message { - font-size: 0.75rem; /* 12px */ - color: var(--error-500); - margin-top: var(--space-1); -} - -/* Textarea */ -textarea.input-field { - height: auto; - min-height: 80px; - padding: var(--space-3) var(--space-4); - resize: vertical; -} - -/* ==================== Card Component ==================== */ -.card { - background-color: var(--bg-primary); - border: var(--border-thin) solid var(--gray-200); - border-radius: var(--radius-medium); - padding: var(--space-4); /* 16px - Mobile */ - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); - transition: all var(--duration-fast) ease-in-out; -} - -@media (min-width: 768px) { - .card { - padding: var(--space-5); /* 20px - Tablet/Desktop */ - } -} - -.card:hover { - box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); -} - -.card-clickable { - cursor: pointer; -} - -.card-clickable:hover { - border-color: var(--primary-500); -} - -.card-header { - display: flex; - align-items: center; - justify-content: space-between; - margin-bottom: var(--space-4); -} - -.card-title { - font-size: 1.125rem; /* 18px */ - font-weight: 600; - color: var(--text-primary); -} - -.card-body { - color: var(--text-secondary); -} - -.card-footer { - display: flex; - align-items: center; - justify-content: space-between; - margin-top: var(--space-4); - padding-top: var(--space-4); - border-top: var(--border-thin) solid var(--gray-200); -} - -/* ==================== Modal Component ==================== */ -.modal-overlay { - position: fixed; - top: 0; - left: 0; - right: 0; - bottom: 0; - background-color: rgba(0, 0, 0, 0.5); - display: flex; - align-items: center; - justify-content: center; - z-index: 1000; - animation: fade-in var(--duration-fast) ease-out; -} - -.modal { - background-color: var(--bg-primary); - border-radius: var(--radius-large); - padding: var(--space-6); - width: 90%; /* Mobile */ - max-width: 480px; - max-height: 90vh; - overflow-y: auto; - box-shadow: 0 20px 25px rgba(0, 0, 0, 0.15); - animation: slide-up var(--duration-normal) ease-out; -} - -@media (min-width: 768px) { - .modal { - width: auto; - max-width: 600px; - } -} - -@media (min-width: 1024px) { - .modal { - max-width: 800px; - } -} - -.modal-header { - display: flex; - align-items: center; - justify-content: space-between; - margin-bottom: var(--space-4); -} - -.modal-title { - font-size: 1.25rem; /* 20px */ - font-weight: 600; - color: var(--text-primary); -} - -.modal-close { - background: none; - border: none; - font-size: 1.5rem; /* 24px */ - color: var(--text-tertiary); - cursor: pointer; - padding: 0; - width: 32px; - height: 32px; - display: flex; - align-items: center; - justify-content: center; - transition: color var(--duration-instant) ease-in-out; -} - -.modal-close:hover { - color: var(--text-primary); -} - -.modal-body { - color: var(--text-secondary); -} - -.modal-footer { - display: flex; - gap: var(--space-3); - justify-content: flex-end; - margin-top: var(--space-6); -} - -/* ==================== Badge Component ==================== */ -.badge { - display: inline-flex; - align-items: center; - gap: var(--space-1); - font-size: 0.75rem; /* 12px */ - font-weight: 500; - padding: var(--space-1) var(--space-2); /* 4px 8px */ - border-radius: var(--radius-full); - white-space: nowrap; -} - -.badge-verified { - background-color: var(--success-50); - color: var(--success-700); -} - -.badge-pending { - background-color: var(--warning-50); - color: var(--warning-700); -} - -.badge-in-progress { - background-color: var(--info-50); - color: var(--info-700); -} - -.badge-confirmed { - background-color: var(--success-50); - color: var(--success-700); - border: var(--border-thin) solid var(--success-500); -} - -.badge-error { - background-color: var(--error-50); - color: var(--error-700); -} - -/* ==================== Progress Bar ==================== */ -.progress-bar { - width: 100%; - height: 4px; - background-color: var(--gray-200); - border-radius: var(--radius-small); - overflow: hidden; -} - -.progress-fill { - height: 100%; - background-color: var(--primary-500); - transition: width var(--duration-slow) ease-out; -} - -.progress-fill.success { - background-color: var(--success-500); -} - -.progress-fill.warning { - background-color: var(--warning-500); -} - -.progress-fill.error { - background-color: var(--error-500); -} - -/* ==================== Spinner ==================== */ -.spinner { - width: 24px; - height: 24px; - border: 3px solid var(--gray-200); - border-top-color: var(--primary-500); - border-radius: 50%; - animation: spin 0.8s linear infinite; -} - -.spinner-small { - width: 16px; - height: 16px; - border-width: 2px; -} - -.spinner-large { - width: 32px; - height: 32px; - border-width: 4px; -} - -/* ==================== Skeleton Loading ==================== */ -.skeleton { - background: linear-gradient( - 90deg, - var(--gray-50) 25%, - var(--gray-100) 50%, - var(--gray-50) 75% - ); - background-size: 200% 100%; - animation: shimmer 1.5s ease-in-out infinite; - border-radius: var(--radius-small); -} - -.skeleton-text { - height: 1rem; - margin-bottom: var(--space-2); -} - -.skeleton-title { - height: 1.5rem; - width: 60%; - margin-bottom: var(--space-3); -} - -.skeleton-avatar { - width: 40px; - height: 40px; - border-radius: 50%; -} - -/* ==================== Toast Component ==================== */ -.toast { - position: fixed; - bottom: 20px; /* Mobile */ - left: 50%; - transform: translateX(-50%); - background-color: var(--gray-900); - color: var(--text-inverse); - padding: var(--space-3) var(--space-4); /* 12px 16px */ - border-radius: var(--radius-medium); - font-size: 0.875rem; /* 14px */ - box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); - z-index: 1000; - animation: toast-in var(--duration-fast) ease-out; - min-width: 280px; - max-width: 90vw; -} - -@media (min-width: 1024px) { - .toast { - bottom: auto; - top: 20px; - right: 20px; - left: auto; - transform: none; - } -} - -.toast-success { - background-color: var(--success-500); -} - -.toast-error { - background-color: var(--error-500); -} - -.toast-info { - background-color: var(--info-500); -} - -.toast-warning { - background-color: var(--warning-500); -} - -/* ==================== Todo Card Component ==================== */ -.todo-card { - background-color: var(--bg-primary); - border: var(--border-thin) solid var(--gray-200); - border-left: var(--border-thick) solid var(--gray-500); - border-radius: var(--radius-medium); - padding: var(--space-3); /* 12px */ - display: flex; - gap: var(--space-3); - align-items: flex-start; - transition: all var(--duration-fast) ease-in-out; -} - -.todo-card:hover { - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); -} - -.todo-card.priority-high { - border-left-color: var(--error-500); -} - -.todo-card.priority-medium { - border-left-color: var(--warning-500); -} - -.todo-card.priority-low { - border-left-color: var(--success-500); -} - -.todo-checkbox { - width: 20px; - height: 20px; - border: var(--border-medium) solid var(--gray-300); - border-radius: var(--radius-small); - cursor: pointer; - flex-shrink: 0; - transition: all var(--duration-instant) ease-in-out; -} - -.todo-checkbox.checked { - background-color: var(--success-500); - border-color: var(--success-500); - background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='white'%3E%3Cpath d='M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z'/%3E%3C/svg%3E"); -} - -.todo-content { - flex: 1; -} - -.todo-title { - font-size: 0.875rem; /* 14px */ - font-weight: 500; - color: var(--text-primary); - margin-bottom: var(--space-1); -} - -.todo-title.completed { - text-decoration: line-through; - color: var(--text-tertiary); -} - -.todo-meta { - display: flex; - flex-wrap: wrap; - gap: var(--space-2); - font-size: 0.75rem; /* 12px */ - color: var(--text-secondary); -} - -.todo-assignee, -.todo-duedate { - display: inline-flex; - align-items: center; - gap: var(--space-1); -} - -.todo-duedate.urgent { - color: var(--error-500); - font-weight: 600; -} - -.todo-meeting-link { - font-size: 0.75rem; /* 12px */ - color: var(--primary-500); - text-decoration: none; - display: inline-flex; - align-items: center; - gap: var(--space-1); - margin-top: var(--space-2); -} - -.todo-meeting-link:hover { - text-decoration: underline; -} - -/* ==================== Voice Recording UI ==================== */ -.voice-recording { - background-color: var(--bg-secondary); - border: var(--border-thin) solid var(--gray-200); - border-radius: var(--radius-medium); - padding: var(--space-4); /* 16px */ - height: 80px; /* Mobile */ - display: flex; - align-items: center; - gap: var(--space-3); -} - -@media (min-width: 1024px) { - .voice-recording { - height: 100px; /* Desktop */ - } -} - -.recording-indicator { - width: 12px; - height: 12px; - background-color: var(--error-500); - border-radius: 50%; - animation: pulse 1.5s ease-in-out infinite; - flex-shrink: 0; -} - -.recording-timer { - font-size: 1.125rem; /* 18px */ - font-weight: 600; - font-family: var(--font-mono); - color: var(--text-primary); - min-width: 60px; -} - -.waveform { - flex: 1; - height: 40px; - display: flex; - align-items: center; - gap: 2px; -} - -.waveform-bar { - width: 3px; - background-color: var(--primary-500); - border-radius: 2px; - animation: wave 1s ease-in-out infinite; -} - -.waveform-bar:nth-child(1) { animation-delay: 0s; } -.waveform-bar:nth-child(2) { animation-delay: 0.1s; } -.waveform-bar:nth-child(3) { animation-delay: 0.2s; } -.waveform-bar:nth-child(4) { animation-delay: 0.3s; } -.waveform-bar:nth-child(5) { animation-delay: 0.4s; } - -/* ==================== Realtime Text Display ==================== */ -.realtime-text { - background-color: var(--bg-primary); - border: var(--border-thin) solid var(--gray-200); - border-radius: var(--radius-medium); - padding: var(--space-3); /* 12px */ - margin-bottom: var(--space-2); -} - -.speaker-name { - display: inline-block; - font-size: 0.75rem; /* 12px */ - font-weight: 600; - color: var(--primary-700); - background-color: var(--primary-50); - padding: var(--space-1) var(--space-2); /* 2px 8px */ - border-radius: var(--radius-small); - margin-bottom: var(--space-1); -} - -.timestamp { - font-size: 0.75rem; /* 12px */ - font-family: var(--font-mono); - color: var(--text-tertiary); - margin-left: var(--space-2); -} - -.text-content { - font-size: 0.875rem; /* 14px */ - line-height: 1.75; - color: var(--gray-700); -} - -.typing-indicator { - display: inline-block; - width: 4px; - height: 16px; - background-color: var(--primary-500); - animation: blink 1s step-end infinite; - margin-left: 2px; -} - -/* ==================== Term Tooltip ==================== */ -.term-highlight { - color: var(--primary-500); - border-bottom: var(--border-thin) dashed var(--primary-500); - cursor: help; - transition: color var(--duration-instant) ease-in-out; -} - -.term-highlight:hover { - color: var(--primary-700); -} - -.tooltip { - position: absolute; - background-color: var(--bg-primary); - border: var(--border-thin) solid var(--gray-200); - border-radius: var(--radius-medium); - padding: var(--space-4); /* 16px */ - width: 320px; /* Mobile */ - max-width: 90vw; - box-shadow: 0 10px 15px rgba(0, 0, 0, 0.1); - z-index: 100; - animation: fade-in var(--duration-fast) ease-out; -} - -@media (min-width: 1024px) { - .tooltip { - width: 400px; /* Desktop */ - } -} - -.tooltip::before { - content: ''; - position: absolute; - bottom: 100%; - left: 20px; - border: 8px solid transparent; - border-bottom-color: var(--gray-200); -} - -.tooltip::after { - content: ''; - position: absolute; - bottom: 100%; - left: 21px; - border: 7px solid transparent; - border-bottom-color: var(--bg-primary); -} - -.tooltip-section { - margin-bottom: var(--space-3); - padding-bottom: var(--space-3); - border-bottom: var(--border-thin) solid var(--gray-200); -} - -.tooltip-section:last-child { - margin-bottom: 0; - padding-bottom: 0; - border-bottom: none; -} - -.tooltip-title { - font-size: 0.75rem; /* 12px */ - font-weight: 600; - color: var(--text-secondary); - margin-bottom: var(--space-1); - text-transform: uppercase; - letter-spacing: 0.05em; -} - -.tooltip-content { - font-size: 0.875rem; /* 14px */ - line-height: 1.5; - color: var(--gray-700); -} - -/* ==================== Accessibility ==================== */ -/* Focus Visible */ -*:focus-visible { - outline: 2px solid var(--primary-500); - outline-offset: 2px; -} - -/* Skip to Main Content */ -.skip-to-main { - position: absolute; - top: -40px; - left: 0; - background: var(--primary-500); - color: white; - padding: var(--space-2) var(--space-4); - text-decoration: none; - z-index: 100; -} - -.skip-to-main:focus { - top: 0; -} - -/* Screen Reader Only */ -.sr-only { - position: absolute; - width: 1px; - height: 1px; - padding: 0; - margin: -1px; - overflow: hidden; - clip: rect(0, 0, 0, 0); - white-space: nowrap; - border-width: 0; -} - -/* ==================== Animations ==================== */ -@keyframes fade-in { - from { - opacity: 0; - } - to { - opacity: 1; - } -} - -@keyframes slide-up { - from { - opacity: 0; - transform: translateY(20px); - } - to { - opacity: 1; - transform: translateY(0); - } -} - -@keyframes slide-down { - from { - opacity: 0; - transform: translateY(-20px); - } - to { - opacity: 1; - transform: translateY(0); - } -} - -@keyframes pulse { - 0%, 100% { - opacity: 1; - } - 50% { - opacity: 0.3; - } -} - -@keyframes spin { - to { - transform: rotate(360deg); - } -} - -@keyframes shimmer { - 0% { - background-position: 200% 0; - } - 100% { - background-position: -200% 0; - } -} - -@keyframes blink { - 0%, 100% { - opacity: 1; - } - 50% { - opacity: 0; - } -} - -@keyframes wave { - 0%, 100% { - height: 10px; - } - 50% { - height: 40px; - } -} - -@keyframes toast-in { - from { - opacity: 0; - transform: translateX(-50%) translateY(20px); - } - to { - opacity: 1; - transform: translateX(-50%) translateY(0); - } -} - -@media (min-width: 1024px) { - @keyframes toast-in { - from { - opacity: 0; - transform: translateX(20px); - } - to { - opacity: 1; - transform: translateX(0); - } - } -} - -@keyframes highlight-fade { - 0% { - background-color: var(--warning-100); - } - 100% { - background-color: transparent; - } -} - -/* ==================== Utility Classes ==================== */ -/* Margin */ -.m-0 { margin: 0; } -.m-1 { margin: var(--space-1); } -.m-2 { margin: var(--space-2); } -.m-3 { margin: var(--space-3); } -.m-4 { margin: var(--space-4); } -.m-5 { margin: var(--space-5); } -.m-6 { margin: var(--space-6); } - -.mt-0 { margin-top: 0; } -.mt-1 { margin-top: var(--space-1); } -.mt-2 { margin-top: var(--space-2); } -.mt-3 { margin-top: var(--space-3); } -.mt-4 { margin-top: var(--space-4); } -.mt-5 { margin-top: var(--space-5); } -.mt-6 { margin-top: var(--space-6); } - -.mb-0 { margin-bottom: 0; } -.mb-1 { margin-bottom: var(--space-1); } -.mb-2 { margin-bottom: var(--space-2); } -.mb-3 { margin-bottom: var(--space-3); } -.mb-4 { margin-bottom: var(--space-4); } -.mb-5 { margin-bottom: var(--space-5); } -.mb-6 { margin-bottom: var(--space-6); } - -.ml-0 { margin-left: 0; } -.ml-1 { margin-left: var(--space-1); } -.ml-2 { margin-left: var(--space-2); } -.ml-3 { margin-left: var(--space-3); } -.ml-4 { margin-left: var(--space-4); } - -.mr-0 { margin-right: 0; } -.mr-1 { margin-right: var(--space-1); } -.mr-2 { margin-right: var(--space-2); } -.mr-3 { margin-right: var(--space-3); } -.mr-4 { margin-right: var(--space-4); } - -/* Padding */ -.p-0 { padding: 0; } -.p-1 { padding: var(--space-1); } -.p-2 { padding: var(--space-2); } -.p-3 { padding: var(--space-3); } -.p-4 { padding: var(--space-4); } -.p-5 { padding: var(--space-5); } -.p-6 { padding: var(--space-6); } - -.pt-0 { padding-top: 0; } -.pt-1 { padding-top: var(--space-1); } -.pt-2 { padding-top: var(--space-2); } -.pt-3 { padding-top: var(--space-3); } -.pt-4 { padding-top: var(--space-4); } - -.pb-0 { padding-bottom: 0; } -.pb-1 { padding-bottom: var(--space-1); } -.pb-2 { padding-bottom: var(--space-2); } -.pb-3 { padding-bottom: var(--space-3); } -.pb-4 { padding-bottom: var(--space-4); } - -/* Text Alignment */ -.text-left { text-align: left; } -.text-center { text-align: center; } -.text-right { text-align: right; } - -/* Display */ -.d-none { display: none; } -.d-block { display: block; } -.d-inline { display: inline; } -.d-inline-block { display: inline-block; } -.d-flex { display: flex; } -.d-inline-flex { display: inline-flex; } - -/* Flexbox */ -.flex-row { flex-direction: row; } -.flex-column { flex-direction: column; } -.flex-wrap { flex-wrap: wrap; } -.flex-nowrap { flex-wrap: nowrap; } - -.justify-start { justify-content: flex-start; } -.justify-center { justify-content: center; } -.justify-end { justify-content: flex-end; } -.justify-between { justify-content: space-between; } -.justify-around { justify-content: space-around; } - -.align-start { align-items: flex-start; } -.align-center { align-items: center; } -.align-end { align-items: flex-end; } -.align-stretch { align-items: stretch; } - -.gap-1 { gap: var(--space-1); } -.gap-2 { gap: var(--space-2); } -.gap-3 { gap: var(--space-3); } -.gap-4 { gap: var(--space-4); } - -/* Width */ -.w-full { width: 100%; } -.w-auto { width: auto; } - -/* Colors */ -.text-primary { color: var(--text-primary); } -.text-secondary { color: var(--text-secondary); } -.text-tertiary { color: var(--text-tertiary); } -.text-inverse { color: var(--text-inverse); } - -.bg-primary { background-color: var(--bg-primary); } -.bg-secondary { background-color: var(--bg-secondary); } - -/* ==================== Responsive Container ==================== */ -.container { - width: 100%; - margin-left: auto; - margin-right: auto; - padding-left: var(--space-4); /* 16px - Mobile */ - padding-right: var(--space-4); -} - -@media (min-width: 768px) { - .container { - padding-left: var(--space-6); /* 24px - Tablet */ - padding-right: var(--space-6); - } -} - -@media (min-width: 1024px) { - .container { - padding-left: var(--space-8); /* 32px - Desktop */ - padding-right: var(--space-8); - max-width: 1440px; - } -} - -/* ==================== Responsive Utilities ==================== */ -/* Mobile Only */ -@media (max-width: 767px) { - .hide-mobile { display: none !important; } -} - -/* Tablet and Up */ -@media (min-width: 768px) { - .hide-tablet { display: none !important; } - .show-mobile { display: none !important; } -} - -/* Desktop Only */ -@media (min-width: 1024px) { - .hide-desktop { display: none !important; } - .show-tablet { display: none !important; } -} diff --git a/design-last/uiux/prototype/common.js b/design-last/uiux/prototype/common.js deleted file mode 100644 index 19af2b5..0000000 --- a/design-last/uiux/prototype/common.js +++ /dev/null @@ -1,1100 +0,0 @@ -/** - * 회의록 작성 및 공유 개선 서비스 - 공통 Javascript - * @version 1.0 - * @author 최유진 (Frontend Developer) - * @date 2025-10-20 - * - * 이 파일은 모든 HTML 프로토타입에서 공통으로 사용되는 유틸리티 함수와 - * 상태 관리, 예제 데이터를 제공합니다. - */ - -/* ============================================================================ - 1. DOM 헬퍼 함수 - ============================================================================ */ - -/** - * 단일 요소 선택 (querySelector 단축) - * @param {string} selector - CSS 선택자 - * @returns {Element|null} - */ -const $ = (selector) => document.querySelector(selector); - -/** - * 복수 요소 선택 (querySelectorAll 단축) - * @param {string} selector - CSS 선택자 - * @returns {NodeList} - */ -const $$ = (selector) => document.querySelectorAll(selector); - -/** - * 엘리먼트 생성 헬퍼 - * @param {string} tag - HTML 태그명 - * @param {string} className - CSS 클래스명 (선택) - * @param {string} text - 텍스트 콘텐츠 (선택) - * @returns {HTMLElement} - */ -function createElement(tag, className = '', text = '') { - const element = document.createElement(tag); - if (className) element.className = className; - if (text) element.textContent = text; - return element; -} - -/** - * 클래스 추가 - * @param {Element} element - 대상 엘리먼트 - * @param {string} className - 추가할 클래스명 - */ -function addClass(element, className) { - if (element) element.classList.add(className); -} - -/** - * 클래스 제거 - * @param {Element} element - 대상 엘리먼트 - * @param {string} className - 제거할 클래스명 - */ -function removeClass(element, className) { - if (element) element.classList.remove(className); -} - -/** - * 클래스 토글 - * @param {Element} element - 대상 엘리먼트 - * @param {string} className - 토글할 클래스명 - */ -function toggleClass(element, className) { - if (element) element.classList.toggle(className); -} - -/* ============================================================================ - 2. 화면 네비게이션 유틸리티 - ============================================================================ */ - -/** - * 화면 이동 함수 - * @param {string} screenName - 이동할 화면 파일명 (예: '02-대시보드.html') - */ -function navigateTo(screenName) { - // 파일 확장자가 없으면 .html 추가 - const fileName = screenName.includes('.html') ? screenName : `${screenName}.html`; - - // 현재 경로 저장 (뒤로가기 용) - const currentPath = window.location.pathname; - sessionStorage.setItem('previousScreen', currentPath); - - // 화면 전환 애니메이션 (선택) - document.body.style.opacity = '0'; - setTimeout(() => { - window.location.href = fileName; - }, 150); -} - -/** - * 뒤로가기 함수 - */ -function goBack() { - const previousScreen = sessionStorage.getItem('previousScreen'); - - if (previousScreen && previousScreen !== window.location.pathname) { - navigateTo(previousScreen); - } else { - // 이전 화면이 없으면 대시보드로 - navigateTo('02-대시보드.html'); - } -} - -/* ============================================================================ - 3. 모달 관리 - ============================================================================ */ - -/** - * 모달 표시 - * @param {string} id - 모달 엘리먼트 ID - */ -function showModal(id) { - const modal = $(`#${id}`); - if (!modal) { - console.error(`Modal with id "${id}" not found`); - return; - } - - modal.style.display = 'flex'; - modal.setAttribute('aria-hidden', 'false'); - - // 애니메이션 - setTimeout(() => { - addClass(modal, 'modal-active'); - }, 10); - - // 배경 스크롤 방지 - document.body.style.overflow = 'hidden'; - - // ESC 키로 닫기 - const handleEscape = (e) => { - if (e.key === 'Escape') { - hideModal(id); - document.removeEventListener('keydown', handleEscape); - } - }; - document.addEventListener('keydown', handleEscape); - - // 포커스 트랩 - trapFocus(modal); -} - -/** - * 모달 숨기기 - * @param {string} id - 모달 엘리먼트 ID - */ -function hideModal(id) { - const modal = $(`#${id}`); - if (!modal) return; - - removeClass(modal, 'modal-active'); - - setTimeout(() => { - modal.style.display = 'none'; - modal.setAttribute('aria-hidden', 'true'); - document.body.style.overflow = ''; - }, 150); -} - -/** - * 모달 외부 클릭 시 닫기 설정 - * @param {string} id - 모달 엘리먼트 ID - */ -function setupModalClickOutside(id) { - const modal = $(`#${id}`); - if (!modal) return; - - modal.addEventListener('click', (e) => { - if (e.target === modal) { - hideModal(id); - } - }); -} - -/** - * 포커스 트랩 (모달 내부에만 포커스 유지) - * @param {Element} container - 포커스를 가둘 컨테이너 - */ -function trapFocus(container) { - const focusableElements = container.querySelectorAll( - 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])' - ); - const firstElement = focusableElements[0]; - const lastElement = focusableElements[focusableElements.length - 1]; - - firstElement?.focus(); - - container.addEventListener('keydown', (e) => { - if (e.key !== 'Tab') return; - - if (e.shiftKey && document.activeElement === firstElement) { - e.preventDefault(); - lastElement?.focus(); - } else if (!e.shiftKey && document.activeElement === lastElement) { - e.preventDefault(); - firstElement?.focus(); - } - }); -} - -/* ============================================================================ - 4. Toast 알림 - ============================================================================ */ - -/** - * 토스트 메시지 표시 - * @param {string} message - 표시할 메시지 - * @param {string} type - 타입 (success|error|info|warning) - * @param {number} duration - 표시 시간 (ms, 기본 3000) - */ -function showToast(message, type = 'info', duration = 3000) { - // 기존 토스트 제거 - const existingToast = $('.toast'); - if (existingToast) { - existingToast.remove(); - } - - // 토스트 생성 - const toast = createElement('div', `toast toast-${type}`, message); - toast.setAttribute('role', 'alert'); - toast.setAttribute('aria-live', 'polite'); - - document.body.appendChild(toast); - - // 애니메이션 - setTimeout(() => { - addClass(toast, 'toast-show'); - }, 10); - - // 자동 제거 - setTimeout(() => { - removeClass(toast, 'toast-show'); - setTimeout(() => { - toast.remove(); - }, 150); - }, duration); -} - -/* ============================================================================ - 5. 폼 검증 - ============================================================================ */ - -/** - * 이메일 형식 검증 - * @param {string} email - 검증할 이메일 - * @returns {boolean} - */ -function validateEmail(email) { - const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; - return emailRegex.test(email); -} - -/** - * 필수 입력 검증 - * @param {string} value - 검증할 값 - * @returns {boolean} - */ -function validateRequired(value) { - return value !== null && value !== undefined && value.trim() !== ''; -} - -/** - * 폼 전체 검증 - * @param {HTMLFormElement} formElement - 검증할 폼 엘리먼트 - * @returns {boolean} - 모든 필드가 유효하면 true - */ -function validateForm(formElement) { - if (!formElement) return false; - - let isValid = true; - const inputs = formElement.querySelectorAll('input[required], textarea[required], select[required]'); - - inputs.forEach(input => { - const value = input.value; - const errorMsg = input.parentElement.querySelector('.input-error-message'); - - // 필수 입력 검증 - if (!validateRequired(value)) { - isValid = false; - addClass(input, 'error'); - - if (errorMsg) { - errorMsg.textContent = `${input.getAttribute('aria-label') || '이 항목'}을 입력해주세요`; - } - } - // 이메일 검증 - else if (input.type === 'email' && !validateEmail(value)) { - isValid = false; - addClass(input, 'error'); - - if (errorMsg) { - errorMsg.textContent = '올바른 이메일을 입력해주세요'; - } - } - // 검증 통과 - else { - removeClass(input, 'error'); - if (errorMsg) { - errorMsg.textContent = ''; - } - } - }); - - return isValid; -} - -/** - * 실시간 입력 검증 설정 - * @param {HTMLInputElement} input - 입력 필드 - */ -function setupRealtimeValidation(input) { - input.addEventListener('blur', () => { - const value = input.value; - const errorMsg = input.parentElement.querySelector('.input-error-message'); - - if (input.hasAttribute('required') && !validateRequired(value)) { - addClass(input, 'error'); - if (errorMsg) { - errorMsg.textContent = `${input.getAttribute('aria-label') || '이 항목'}을 입력해주세요`; - } - } else if (input.type === 'email' && !validateEmail(value)) { - addClass(input, 'error'); - if (errorMsg) { - errorMsg.textContent = '올바른 이메일을 입력해주세요'; - } - } else { - removeClass(input, 'error'); - if (errorMsg) { - errorMsg.textContent = ''; - } - } - }); - - input.addEventListener('input', () => { - removeClass(input, 'error'); - }); -} - -/* ============================================================================ - 6. 로컬 스토리지 관리 - ============================================================================ */ - -/** - * 데이터 저장 - * @param {string} key - 저장할 키 - * @param {any} value - 저장할 값 - */ -function saveData(key, value) { - try { - const jsonValue = JSON.stringify(value); - localStorage.setItem(key, jsonValue); - } catch (error) { - console.error('데이터 저장 실패:', error); - } -} - -/** - * 데이터 로드 - * @param {string} key - 로드할 키 - * @returns {any} - 저장된 값 (없으면 null) - */ -function loadData(key) { - try { - const jsonValue = localStorage.getItem(key); - return jsonValue ? JSON.parse(jsonValue) : null; - } catch (error) { - console.error('데이터 로드 실패:', error); - return null; - } -} - -/** - * 데이터 삭제 - * @param {string} key - 삭제할 키 - */ -function removeData(key) { - try { - localStorage.removeItem(key); - } catch (error) { - console.error('데이터 삭제 실패:', error); - } -} - -/* ============================================================================ - 7. 날짜/시간 유틸리티 - ============================================================================ */ - -/** - * 날짜 포맷 (YYYY-MM-DD) - * @param {Date|string} date - 포맷할 날짜 - * @returns {string} - */ -function formatDate(date) { - const d = new Date(date); - const year = d.getFullYear(); - const month = String(d.getMonth() + 1).padStart(2, '0'); - const day = String(d.getDate()).padStart(2, '0'); - return `${year}-${month}-${day}`; -} - -/** - * 시간 포맷 (HH:MM) - * @param {Date|string} time - 포맷할 시간 - * @returns {string} - */ -function formatTime(time) { - const d = new Date(time); - const hours = String(d.getHours()).padStart(2, '0'); - const minutes = String(d.getMinutes()).padStart(2, '0'); - return `${hours}:${minutes}`; -} - -/** - * 날짜/시간 포맷 (YYYY-MM-DD HH:MM) - * @param {Date|string} datetime - 포맷할 날짜/시간 - * @returns {string} - */ -function formatDateTime(datetime) { - return `${formatDate(datetime)} ${formatTime(datetime)}`; -} - -/** - * D-day 계산 - * @param {Date|string} targetDate - 목표 날짜 - * @returns {string} - 예: 'D-5', 'D-day', 'D+3' - */ -function getDDay(targetDate) { - const today = new Date(); - today.setHours(0, 0, 0, 0); - - const target = new Date(targetDate); - target.setHours(0, 0, 0, 0); - - const diffTime = target - today; - const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)); - - if (diffDays === 0) return 'D-day'; - if (diffDays > 0) return `D-${diffDays}`; - return `D+${Math.abs(diffDays)}`; -} - -/** - * 상대 시간 표시 (예: 5분 전, 2시간 전) - * @param {Date|string} date - 날짜 - * @returns {string} - */ -function getRelativeTime(date) { - const now = new Date(); - const past = new Date(date); - const diffMs = now - past; - const diffMins = Math.floor(diffMs / (1000 * 60)); - const diffHours = Math.floor(diffMs / (1000 * 60 * 60)); - const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24)); - - if (diffMins < 1) return '방금 전'; - if (diffMins < 60) return `${diffMins}분 전`; - if (diffHours < 24) return `${diffHours}시간 전`; - if (diffDays < 7) return `${diffDays}일 전`; - return formatDate(date); -} - -/* ============================================================================ - 8. 이벤트 처리 - ============================================================================ */ - -/** - * 디바운싱 (연속된 이벤트를 지연 처리) - * @param {Function} func - 실행할 함수 - * @param {number} delay - 지연 시간 (ms) - * @returns {Function} - */ -function debounce(func, delay = 300) { - let timeoutId; - return function (...args) { - clearTimeout(timeoutId); - timeoutId = setTimeout(() => func.apply(this, args), delay); - }; -} - -/** - * 쓰로틀링 (일정 시간마다 한 번만 실행) - * @param {Function} func - 실행할 함수 - * @param {number} limit - 제한 시간 (ms) - * @returns {Function} - */ -function throttle(func, limit = 100) { - let inThrottle; - return function (...args) { - if (!inThrottle) { - func.apply(this, args); - inThrottle = true; - setTimeout(() => { - inThrottle = false; - }, limit); - } - }; -} - -/* ============================================================================ - 9. 애니메이션 헬퍼 - ============================================================================ */ - -/** - * 페이드 인 애니메이션 - * @param {Element} element - 대상 엘리먼트 - * @param {number} duration - 지속 시간 (ms, 기본 150) - */ -function fadeIn(element, duration = 150) { - if (!element) return; - - element.style.opacity = '0'; - element.style.display = 'block'; - - let start = null; - function animate(timestamp) { - if (!start) start = timestamp; - const progress = timestamp - start; - - element.style.opacity = Math.min(progress / duration, 1); - - if (progress < duration) { - requestAnimationFrame(animate); - } - } - - requestAnimationFrame(animate); -} - -/** - * 페이드 아웃 애니메이션 - * @param {Element} element - 대상 엘리먼트 - * @param {number} duration - 지속 시간 (ms, 기본 150) - */ -function fadeOut(element, duration = 150) { - if (!element) return; - - let start = null; - function animate(timestamp) { - if (!start) start = timestamp; - const progress = timestamp - start; - - element.style.opacity = 1 - Math.min(progress / duration, 1); - - if (progress < duration) { - requestAnimationFrame(animate); - } else { - element.style.display = 'none'; - } - } - - requestAnimationFrame(animate); -} - -/** - * 슬라이드 업 애니메이션 - * @param {Element} element - 대상 엘리먼트 - * @param {number} duration - 지속 시간 (ms, 기본 200) - */ -function slideUp(element, duration = 200) { - if (!element) return; - - const height = element.offsetHeight; - element.style.overflow = 'hidden'; - element.style.transition = `height ${duration}ms ease-out`; - element.style.height = `${height}px`; - - setTimeout(() => { - element.style.height = '0'; - }, 10); - - setTimeout(() => { - element.style.display = 'none'; - element.style.height = ''; - element.style.overflow = ''; - element.style.transition = ''; - }, duration); -} - -/** - * 슬라이드 다운 애니메이션 - * @param {Element} element - 대상 엘리먼트 - * @param {number} duration - 지속 시간 (ms, 기본 200) - */ -function slideDown(element, duration = 200) { - if (!element) return; - - element.style.display = 'block'; - const height = element.offsetHeight; - - element.style.overflow = 'hidden'; - element.style.height = '0'; - element.style.transition = `height ${duration}ms ease-out`; - - setTimeout(() => { - element.style.height = `${height}px`; - }, 10); - - setTimeout(() => { - element.style.height = ''; - element.style.overflow = ''; - element.style.transition = ''; - }, duration); -} - -/* ============================================================================ - 10. 회의록 예제 데이터 - ============================================================================ */ - -/** - * 참석자 예제 데이터 - */ -const mockUsers = [ - { id: 1, name: '김민준', email: 'minjun.kim@company.com', role: 'Product Owner', avatar: '👨‍💼' }, - { id: 2, name: '박서연', email: 'seoyeon.park@company.com', role: 'AI Specialist', avatar: '👩‍💻' }, - { id: 3, name: '이준호', email: 'junho.lee@company.com', role: 'Backend Developer', avatar: '👨‍💻' }, - { id: 4, name: '최유진', email: 'yujin.choi@company.com', role: 'Frontend Developer', avatar: '👩‍🎨' }, - { id: 5, name: '정도현', email: 'dohyun.jung@company.com', role: 'QA Engineer', avatar: '👨‍🔬' } -]; - -/** - * 회의록 예제 데이터 - */ -const mockMeetings = [ - { - id: 1, - title: '프로젝트 킥오프', - date: '2025-10-20', - time: '14:00', - status: 'confirmed', // confirmed | in-progress | draft - attendees: [1, 2, 3, 4, 5], - location: '회의실 A', - template: 'kickoff', - progress: 100, - sections: [ - { - title: '참석자', - content: '김민준 (주관자), 박서연, 이준호, 최유진, 정도현', - verified: true, - verifiedBy: 1, - verifiedAt: '2025-10-20 14:45' - }, - { - title: '안건', - content: '- 프로젝트 목표 정의\n- 일정 및 마일스톤\n- 역할 분담', - verified: true - }, - { - title: '논의 내용', - content: 'Q1까지 MVP 완성을 목표로 설정. React 프레임워크와 AWS 인프라 사용 결정.', - verified: true - }, - { - title: '결정 사항', - content: '- 개발 프레임워크: React\n- 배포 환경: AWS\n- 주간 회의: 매주 월요일 10시', - verified: true - } - ], - todos: [ - { - id: 1, - content: '요구사항 정의서 작성', - assignee: 1, - dueDate: '2025-10-25', - priority: 'high', - status: 'pending' - }, - { - id: 2, - content: '기술 스택 상세 검토', - assignee: 2, - dueDate: '2025-10-27', - priority: 'medium', - status: 'pending' - } - ], - keywords: ['MVP', 'React', 'AWS'] - }, - { - id: 2, - title: '주간 회의', - date: '2025-10-19', - time: '10:00', - status: 'in-progress', - attendees: [1, 2, 3, 4, 5], - location: '온라인', - template: 'weekly', - progress: 60, - sections: [ - { - title: '참석자', - content: '김민준, 박서연, 이준호, 최유진, 정도현', - verified: true - }, - { - title: '주간 실적', - content: '- UI 프로토타입 완성\n- API 설계 초안 작성', - verified: false - }, - { - title: '주요 이슈', - content: '데이터베이스 스키마 변경 필요', - verified: false - } - ], - todos: [ - { - id: 3, - content: 'DB 스키마 수정', - assignee: 3, - dueDate: '2025-10-22', - priority: 'high', - status: 'pending' - } - ], - keywords: ['주간회의', 'UI', 'API'] - }, - { - id: 3, - title: '스프린트 계획 회의', - date: '2025-10-18', - time: '14:30', - status: 'confirmed', - attendees: [1, 2, 3, 4], - location: '회의실 B', - template: 'scrum', - progress: 100, - sections: [ - { - title: '참석자', - content: '김민준, 박서연, 이준호, 최유진', - verified: true - }, - { - title: '어제 한 일', - content: '각 팀원별 작업 진행 상황 공유', - verified: true - }, - { - title: '오늘 할 일', - content: '스프린트 목표 설정 및 작업 할당', - verified: true - } - ], - todos: [], - keywords: ['스프린트', '계획'] - }, - { - id: 4, - title: '기술 검토 회의', - date: '2025-10-17', - time: '15:00', - status: 'confirmed', - attendees: [2, 3, 4], - location: '온라인', - template: 'general', - progress: 100, - sections: [ - { - title: '참석자', - content: '박서연, 이준호, 최유진', - verified: true - }, - { - title: '논의 내용', - content: 'AI 모델 선택 및 RAG 시스템 구조 검토', - verified: true - } - ], - todos: [ - { - id: 4, - content: 'AI 모델 벤치마크 테스트', - assignee: 2, - dueDate: '2025-10-24', - priority: 'high', - status: 'completed', - completedAt: '2025-10-18' - } - ], - keywords: ['AI', 'RAG', '기술검토'] - }, - { - id: 5, - title: '디자인 리뷰', - date: '2025-10-16', - time: '11:00', - status: 'confirmed', - attendees: [1, 4, 5], - location: '회의실 C', - template: 'general', - progress: 100, - sections: [ - { - title: '참석자', - content: '김민준, 최유진, 정도현', - verified: true - }, - { - title: '논의 내용', - content: 'UI/UX 설계 및 프로토타입 리뷰', - verified: true - }, - { - title: '결정 사항', - content: 'Mobile First 접근 방식 채택', - verified: true - } - ], - todos: [ - { - id: 5, - content: '프로토타입 수정', - assignee: 4, - dueDate: '2025-10-20', - priority: 'medium', - status: 'completed', - completedAt: '2025-10-19' - } - ], - keywords: ['디자인', 'UI', 'UX'] - } -]; - -/** - * Todo 예제 데이터 - */ -const mockTodos = [ - { - id: 1, - content: '요구사항 정의서 작성', - assignee: 1, - assigneeName: '김민준', - dueDate: '2025-10-25', - priority: 'high', - status: 'pending', - meetingId: 1, - meetingTitle: '프로젝트 킥오프', - meetingDate: '2025-10-20' - }, - { - id: 2, - content: '기술 스택 상세 검토', - assignee: 2, - assigneeName: '박서연', - dueDate: '2025-10-27', - priority: 'medium', - status: 'pending', - meetingId: 1, - meetingTitle: '프로젝트 킥오프', - meetingDate: '2025-10-20' - }, - { - id: 3, - content: 'DB 스키마 수정', - assignee: 3, - assigneeName: '이준호', - dueDate: '2025-10-22', - priority: 'high', - status: 'pending', - meetingId: 2, - meetingTitle: '주간 회의', - meetingDate: '2025-10-19' - }, - { - id: 4, - content: 'AI 모델 벤치마크 테스트', - assignee: 2, - assigneeName: '박서연', - dueDate: '2025-10-24', - priority: 'high', - status: 'completed', - completedAt: '2025-10-18', - meetingId: 4, - meetingTitle: '기술 검토 회의', - meetingDate: '2025-10-17' - }, - { - id: 5, - content: '프로토타입 수정', - assignee: 4, - assigneeName: '최유진', - dueDate: '2025-10-20', - priority: 'medium', - status: 'completed', - completedAt: '2025-10-19', - meetingId: 5, - meetingTitle: '디자인 리뷰', - meetingDate: '2025-10-16' - } -]; - -/** - * 사용자별 Todo 가져오기 - * @param {number} userId - 사용자 ID - * @returns {Array} - */ -function getTodosByUser(userId) { - return mockTodos.filter(todo => todo.assignee === userId); -} - -/** - * 회의별 Todo 가져오기 - * @param {number} meetingId - 회의 ID - * @returns {Array} - */ -function getTodosByMeeting(meetingId) { - return mockTodos.filter(todo => todo.meetingId === meetingId); -} - -/** - * 사용자 정보 가져오기 - * @param {number} userId - 사용자 ID - * @returns {Object|null} - */ -function getUserById(userId) { - return mockUsers.find(user => user.id === userId) || null; -} - -/** - * 회의록 정보 가져오기 - * @param {number} meetingId - 회의 ID - * @returns {Object|null} - */ -function getMeetingById(meetingId) { - return mockMeetings.find(meeting => meeting.id === meetingId) || null; -} - -/* ============================================================================ - 11. 초기화 함수 - ============================================================================ */ - -/** - * 공통 이벤트 리스너 등록 및 초기화 - */ -function initCommon() { - // 페이지 로드 시 페이드인 - document.body.style.opacity = '1'; - - // 뒤로가기 버튼 이벤트 - const backButtons = $$('[data-action="back"]'); - backButtons.forEach(btn => { - btn.addEventListener('click', goBack); - }); - - // 모달 외부 클릭 이벤트 - const modals = $$('.modal-overlay'); - modals.forEach(modal => { - setupModalClickOutside(modal.id); - }); - - // 모달 닫기 버튼 - const closeButtons = $$('.modal-close'); - closeButtons.forEach(btn => { - btn.addEventListener('click', (e) => { - const modal = e.target.closest('.modal-overlay'); - if (modal) { - hideModal(modal.id); - } - }); - }); - - // 폼 실시간 검증 - const inputs = $$('input[required], input[type="email"]'); - inputs.forEach(input => { - setupRealtimeValidation(input); - }); - - // 네비게이션 링크 활성화 표시 - highlightCurrentPage(); - - // 로컬 스토리지 초기화 (첫 방문 시) - initLocalStorage(); - - console.log('Common.js initialized'); -} - -/** - * 현재 페이지 네비게이션 하이라이트 - */ -function highlightCurrentPage() { - const currentPath = window.location.pathname; - const navLinks = $$('[data-nav-link]'); - - navLinks.forEach(link => { - const href = link.getAttribute('href'); - if (currentPath.includes(href)) { - addClass(link, 'active'); - } - }); -} - -/** - * 로컬 스토리지 초기화 (첫 방문 시) - */ -function initLocalStorage() { - if (!loadData('initialized')) { - // 현재 사용자 설정 (예제: 김민준) - saveData('currentUser', { id: 1, name: '김민준', email: 'minjun.kim@company.com' }); - - // 회의록 데이터 - saveData('meetings', mockMeetings); - - // Todo 데이터 - saveData('todos', mockTodos); - - // 초기화 완료 플래그 - saveData('initialized', true); - - console.log('Local storage initialized with mock data'); - } -} - -/** - * 현재 로그인 사용자 가져오기 - * @returns {Object|null} - */ -function getCurrentUser() { - return loadData('currentUser'); -} - -/* ============================================================================ - 12. DOM 로드 완료 시 초기화 - ============================================================================ */ - -// DOM 로드 완료 시 공통 초기화 실행 -if (document.readyState === 'loading') { - document.addEventListener('DOMContentLoaded', initCommon); -} else { - initCommon(); -} - -/* ============================================================================ - 전역 스코프에 함수 노출 (HTML에서 직접 호출 가능) - ============================================================================ */ - -window.commonUtils = { - // DOM - $, - $$, - createElement, - addClass, - removeClass, - toggleClass, - - // Navigation - navigateTo, - goBack, - - // Modal - showModal, - hideModal, - setupModalClickOutside, - - // Toast - showToast, - - // Validation - validateEmail, - validateRequired, - validateForm, - setupRealtimeValidation, - - // Storage - saveData, - loadData, - removeData, - - // Date/Time - formatDate, - formatTime, - formatDateTime, - getDDay, - getRelativeTime, - - // Events - debounce, - throttle, - - // Animation - fadeIn, - fadeOut, - slideUp, - slideDown, - - // Mock Data - mockUsers, - mockMeetings, - mockTodos, - getTodosByUser, - getTodosByMeeting, - getUserById, - getMeetingById, - getCurrentUser, - - // Init - initCommon -}; diff --git a/design-last/uiux/uiux.md b/design-last/uiux/uiux.md deleted file mode 100644 index 2fb0138..0000000 --- a/design-last/uiux/uiux.md +++ /dev/null @@ -1,1558 +0,0 @@ -# 회의록 작성 및 공유 개선 서비스 - UI/UX 설계서 - -## 문서 정보 -- **작성일**: 2025-10-20 -- **작성자**: 이미준 (서비스 기획자) -- **버전**: 1.0 -- **설계 원칙**: Mobile First 디자인 철학 -- **접근성 기준**: WCAG 2.1 Level AA - ---- - -## 목차 -1. [프로토타입 화면 목록](#1-프로토타입-화면-목록) -2. [화면 간 사용자 플로우](#2-화면-간-사용자-플로우) -3. [화면별 상세 설계](#3-화면별-상세-설계) -4. [화면 간 전환 및 네비게이션](#4-화면-간-전환-및-네비게이션) -5. [반응형 설계 전략](#5-반응형-설계-전략) -6. [접근성 보장 방안](#6-접근성-보장-방안) -7. [성능 최적화 방안](#7-성능-최적화-방안) -8. [변경 이력](#8-변경-이력) - ---- - -## 1. 프로토타입 화면 목록 - -본 서비스는 Mobile First 설계 원칙에 따라 총 9개의 핵심 화면으로 구성됩니다. - -| 번호 | 화면명 | 관련 유저스토리 | 비즈니스 중요도 | 비고 | -|------|--------|-----------------|-----------------|------| -| 01 | 로그인 | UFR-USER-010 | 필수 | 인증 진입점 | -| 02 | 대시보드 | UFR-MEET-045, UFR-MEET-055 | 높음 | 메인 화면 | -| 03 | 회의예약 | UFR-MEET-010 | 높음 | 회의 준비 | -| 04 | 템플릿선택 | UFR-MEET-020 | 중간 | 회의록 템플릿 | -| 05 | 회의진행 | UFR-MEET-030, UFR-AI-010, UFR-RAG-010/020, UFR-COLLAB-010/020 | 매우 높음 | 핵심 차별화 기능 | -| 06 | 검증완료 | UFR-COLLAB-030 | 중간 | 품질 보장 | -| 07 | 회의종료 | UFR-MEET-040, UFR-MEET-050, UFR-AI-020 | 높음 | 회의록 확정 | -| 08 | 회의록공유 | UFR-MEET-060 | 높음 | 공유 및 협업 | -| 09 | Todo관리 | UFR-TODO-010, UFR-TODO-030 | 높음 | 차별화 기능 | - ---- - -## 2. 화면 간 사용자 플로우 - -### 2.1 주요 사용자 여정 - -``` -[인증 플로우] -01-로그인 → 02-대시보드 - -[회의 예약 플로우] -02-대시보드 → 03-회의예약 → 02-대시보드 - -[회의 진행 플로우] -02-대시보드 → 04-템플릿선택 → 05-회의진행 → 06-검증완료 → 07-회의종료 → 08-회의록공유 - -[회의록 관리 플로우] -02-대시보드 → 회의록 상세 조회 → 수정/공유/다운로드 - -[Todo 관리 플로우] -09-Todo관리 → Todo 완료/수정 → 회의록 자동 반영 -``` - -### 2.2 플로우 다이어그램 - -``` -┌─────────┐ -│01-로그인│ -└────┬────┘ - │ - ▼ -┌──────────┐ ┌──────────┐ -│02-대시보드│────▶│03-회의예약│ -└────┬─────┘ └──────────┘ - │ - ├─────────▶┌──────────────┐ - │ │04-템플릿선택 │ - │ └──────┬───────┘ - │ │ - │ ▼ - │ ┌──────────┐ - │ │05-회의진행│ - │ └────┬─────┘ - │ │ - │ ▼ - │ ┌──────────┐ - │ │06-검증완료│ - │ └────┬─────┘ - │ │ - │ ▼ - │ ┌──────────┐ - │ │07-회의종료│ - │ └────┬─────┘ - │ │ - │ ▼ - │ ┌────────────┐ - │ │08-회의록공유│ - │ └────────────┘ - │ - └─────────▶┌──────────┐ - │09-Todo관리│ - └──────────┘ -``` - ---- - -## 3. 화면별 상세 설계 - -### 3.1 01-로그인 - -#### 개요 -- **목적**: 사용자 인증 및 서비스 진입 -- **관련 유저스토리**: UFR-USER-010 -- **비즈니스 중요도**: 필수 -- **화면 타입**: 단일 목적 페이지 - -#### 주요 기능 -1. 사번과 비밀번호를 통한 LDAP 인증 -2. 세션 관리 및 보안 유지 -3. 인증 실패 시 오류 메시지 표시 - -#### UI 구성요소 - -**Mobile (320px ~ 767px)** -``` -┌─────────────────────────┐ -│ [로고 이미지] │ -│ │ -│ 회의록 작성 서비스 │ -│ │ -│ ┌───────────────────┐ │ -│ │ 사번 │ │ -│ │ [입력 필드] │ │ -│ └───────────────────┘ │ -│ │ -│ ┌───────────────────┐ │ -│ │ 비밀번호 │ │ -│ │ [입력 필드] │ │ -│ └───────────────────┘ │ -│ │ -│ ┌───────────────────┐ │ -│ │ 로그인 버튼 │ │ -│ └───────────────────┘ │ -│ │ -│ LDAP 연동 인증 시스템 │ -└─────────────────────────┘ -``` - -**주요 컴포넌트**: -- 로고 이미지 (SVG, 반응형) -- 사번 입력 필드 (type="text", autocomplete="username") -- 비밀번호 입력 필드 (type="password", autocomplete="current-password") -- 로그인 버튼 (primary action, 44x44px 이상) -- 인증 안내 텍스트 - -#### 인터랙션 -1. **입력 검증**: - - 실시간 입력 검증 (사번 형식, 비밀번호 입력 여부) - - 포커스 이동: Tab 키로 필드 간 이동 - - Enter 키로 로그인 실행 - -2. **로그인 처리**: - - 버튼 클릭 → 로딩 인디케이터 표시 - - LDAP 인증 진행 - - 성공 시: 대시보드로 자동 이동 - - 실패 시: 오류 메시지 표시 (3초 후 자동 사라짐) - -3. **오류 처리**: - - 인증 실패: "사번 또는 비밀번호가 올바르지 않습니다" - - 네트워크 오류: "연결에 실패했습니다. 다시 시도해주세요" - - 서버 오류: "일시적인 오류가 발생했습니다" - -#### 데이터 요구사항 -- **입력**: 사번 (문자열), 비밀번호 (문자열) -- **출력**: 인증 토큰, 사용자 정보 (이름, 권한) -- **저장**: 세션 토큰 (로컬 스토리지/세션 스토리지) - -#### 에러 처리 -| 에러 유형 | 메시지 | 액션 | -|-----------|--------|------| -| 인증 실패 | 사번 또는 비밀번호가 올바르지 않습니다 | 재입력 유도 | -| 네트워크 오류 | 연결에 실패했습니다 | 재시도 버튼 표시 | -| 서버 오류 | 일시적인 오류가 발생했습니다 | 관리자 문의 안내 | - ---- - -### 3.2 02-대시보드 - -#### 개요 -- **목적**: 회의록 목록 조회, 빠른 액세스, 상태 확인 -- **관련 유저스토리**: UFR-MEET-045 (상세조회), UFR-MEET-055 (수정) -- **비즈니스 중요도**: 높음 -- **화면 타입**: 목록 및 액션 페이지 - -#### 주요 기능 -1. 회의록 목록 조회 (필터링, 정렬, 검색) -2. 빠른 액션 (새 회의 예약, 진행 중인 회의) -3. 회의록 상세 조회 및 수정 -4. 알림 확인 - -#### UI 구성요소 - -**Mobile (320px ~ 767px)** -``` -┌─────────────────────────────┐ -│ [프로필] 대시보드 [알림🔔] │ -├─────────────────────────────┤ -│ │ -│ ┌─────────────────────┐ │ -│ │ ➕ 새 회의 예약 │ │ -│ └─────────────────────┘ │ -│ │ -│ 🔴 진행 중인 회의 (1건) │ -│ [회의 제목 - 지금 참여] │ -│ │ -├─────────────────────────────┤ -│ 내 회의록 │ -│ │ -│ [전체 ▼] [최신순 ▼] [🔍] │ -│ │ -│ ┌───────────────────────┐ │ -│ │ 📝 프로젝트 킥오프 │ │ -│ │ 2025-10-20 14:00 │ │ -│ │ ✅ 확정완료 │ │ -│ │ 3명 참석 │ │ -│ └───────────────────────┘ │ -│ │ -│ ┌───────────────────────┐ │ -│ │ 📝 주간 회의 │ │ -│ │ 2025-10-19 10:00 │ │ -│ │ ⚠️ 작성중 (60%) │ │ -│ │ 5명 참석 │ │ -│ └───────────────────────┘ │ -│ │ -│ [더보기...] │ -│ │ -├─────────────────────────────┤ -│ [대시보드] [Todo] [더보기]│ -└─────────────────────────────┘ -``` - -**주요 컴포넌트**: -- 상단 헤더: 프로필 아이콘, 타이틀, 알림 아이콘 -- 빠른 액션 버튼: 새 회의 예약 (primary), 진행 중인 회의 (secondary) -- 필터 영역: 상태 필터 (전체/작성중/확정완료), 정렬 (최신순/회의일시순/제목순) -- 검색 바: 회의 제목, 참석자, 키워드 검색 -- 회의록 카드: 제목, 날짜, 상태, 참석자 수, 진행률 (작성중인 경우) -- 하단 네비게이션: 대시보드, Todo, 더보기 - -#### 인터랙션 -1. **회의록 목록**: - - 무한 스크롤 또는 페이지네이션 - - 카드 탭: 상세 화면으로 이동 - - 스와이프: 빠른 액션 (공유, 삭제) - -2. **필터링 및 검색**: - - 필터 선택: 즉시 목록 갱신 - - 검색: 300ms 디바운싱 후 API 요청 - - 결과 없을 시: "검색 결과가 없습니다" 메시지 - -3. **빠른 액션**: - - 새 회의 예약: 03-회의예약 화면으로 이동 - - 진행 중인 회의: 05-회의진행 화면으로 복귀 - -4. **회의록 상세 조회** (UFR-MEET-045): - - 카드 클릭 → 상세 화면 모달 또는 새 페이지 - - 표시 정보: - - 회의 기본 정보: 제목, 일시, 장소, 참석자, 템플릿, 상태, 작성자 - - 섹션별 내용: 논의사항, 결정사항, Todo, 기타 - - 검증 상태: 섹션별 체크 표시 - - Todo 항목: 담당자, 마감일, 상태, 우선순위 - - 첨부파일: 다운로드 링크 - - 부가 기능: - - 수정 버튼 (권한 있는 경우) - - 공유 버튼 - - PDF 다운로드 - - 이전/다음 회의록 네비게이션 - - 뒤로가기 - -5. **회의록 수정** (UFR-MEET-055): - - 수정 버튼 클릭 → 편집 모드 전환 - - 상태별 수정 범위: - - 작성중: 모든 섹션 수정 가능 - - 확정완료: 승인 요청 후 수정 가능 - - 자동 저장: 30초 간격 - - 수정 이력: 누가, 언제, 무엇을 수정했는지 - - 저장 버튼 클릭 → 상태 업데이트 → 목록 갱신 - -#### 데이터 요구사항 -- **입력**: 필터 조건, 검색 키워드, 정렬 옵션 -- **출력**: 회의록 목록 (제목, 날짜, 상태, 참석자, 진행률) -- **캐싱**: 최근 조회한 목록 (5분) - -#### 에러 처리 -| 에러 유형 | 메시지 | 액션 | -|-----------|--------|------| -| 조회 실패 | 목록을 불러올 수 없습니다 | 새로고침 버튼 | -| 검색 실패 | 검색 중 오류가 발생했습니다 | 재시도 유도 | -| 수정 권한 없음 | 수정 권한이 없습니다 | 확인 버튼 | - ---- - -### 3.3 03-회의예약 - -#### 개요 -- **목적**: 회의 예약 및 참석자 초대 -- **관련 유저스토리**: UFR-MEET-010 -- **비즈니스 중요도**: 높음 -- **화면 타입**: 폼 입력 페이지 - -#### 주요 기능 -1. 회의 정보 입력 (제목, 날짜/시간, 장소, 참석자) -2. 입력 검증 및 예약 생성 -3. 초대 이메일 자동 발송 -4. 캘린더 자동 등록 - -#### UI 구성요소 - -**Mobile (320px ~ 767px)** -``` -┌─────────────────────────────┐ -│ [←] 회의 예약 [저장]│ -├─────────────────────────────┤ -│ │ -│ 회의 제목 * │ -│ ┌─────────────────────┐ │ -│ │ [입력 필드] │ │ -│ └─────────────────────┘ │ -│ │ -│ 날짜 및 시간 * │ -│ ┌──────────┬──────────┐ │ -│ │ 2025-10-20│ 14:00 │ │ -│ └──────────┴──────────┘ │ -│ │ -│ 장소 (선택) │ -│ ┌─────────────────────┐ │ -│ │ [입력 필드] │ │ -│ └─────────────────────┘ │ -│ │ -│ 참석자 * │ -│ ┌─────────────────────┐ │ -│ │ user1@company.com │ │ -│ │ user2@company.com │ │ -│ │ + 참석자 추가 │ │ -│ └─────────────────────┘ │ -│ │ -│ ☑️ 회의 시작 30분 전 │ -│ 리마인더 발송 │ -│ │ -│ ┌─────────────────────┐ │ -│ │ 회의 예약하기 │ │ -│ └─────────────────────┘ │ -│ │ -└─────────────────────────────┘ -``` - -**주요 컴포넌트**: -- 상단 헤더: 뒤로가기, 타이틀, 저장 버튼 -- 회의 제목 입력 필드 (최대 100자, 필수) -- 날짜 선택기 (달력 UI, 필수) -- 시간 선택기 (시간 목록, 필수) -- 장소 입력 필드 (최대 200자, 선택) -- 참석자 입력 영역 (이메일 칩, 최소 1명 필수) -- 리마인더 설정 체크박스 -- 예약하기 버튼 (primary action) - -#### 인터랙션 -1. **폼 입력**: - - 실시간 검증: 제목 길이, 이메일 형식, 날짜/시간 유효성 - - 참석자 추가: 이메일 입력 후 Enter 또는 버튼 클릭 - - 참석자 제거: 칩 클릭 또는 스와이프 - -2. **날짜/시간 선택**: - - 날짜 선택기: 달력 모달 표시 - - 시간 선택기: 드롭다운 또는 스크롤 선택 - - 과거 날짜 선택 불가 - -3. **예약 처리**: - - 버튼 클릭 → 필수 항목 검증 - - 성공 시: - - 회의 ID 생성 - - 캘린더 자동 등록 - - 참석자에게 이메일 발송 - - 성공 메시지 표시 (토스트) - - 대시보드로 이동 - - 실패 시: 오류 메시지 표시 - -4. **자동 저장**: - - 입력 중 임시 저장 (30초 간격) - - 뒤로가기 시 저장 확인 다이얼로그 - -#### 데이터 요구사항 -- **입력**: 제목, 날짜, 시간, 장소, 참석자 목록 -- **출력**: 회의 ID, 예약 확인 -- **저장**: 임시 저장 데이터 (로컬 스토리지) - -#### 에러 처리 -| 에러 유형 | 메시지 | 액션 | -|-----------|--------|------| -| 필수 항목 누락 | [항목명]을 입력해주세요 | 해당 필드로 포커스 | -| 이메일 형식 오류 | 올바른 이메일을 입력해주세요 | 재입력 유도 | -| 예약 실패 | 예약에 실패했습니다 | 재시도 버튼 | -| 날짜 과거 선택 | 과거 날짜는 선택할 수 없습니다 | 오늘 날짜로 초기화 | - ---- - -### 3.4 04-템플릿선택 - -#### 개요 -- **목적**: 회의 유형에 맞는 템플릿 선택 및 커스터마이징 -- **관련 유저스토리**: UFR-MEET-020 -- **비즈니스 중요도**: 중간 -- **화면 타입**: 선택 및 설정 페이지 - -#### 주요 기능 -1. 템플릿 목록 표시 (일반, 스크럼, 킥오프, 주간) -2. 템플릿 미리보기 -3. 섹션 커스터마이징 (추가/삭제/순서 변경) -4. 회의록 도구 준비 - -#### UI 구성요소 - -**Mobile (320px ~ 767px)** -``` -┌─────────────────────────────┐ -│ [←] 템플릿 선택 [다음]│ -├─────────────────────────────┤ -│ │ -│ 회의 유형에 맞는 템플릿을 │ -│ 선택해주세요 │ -│ │ -│ ┌───────────────────────┐ │ -│ │ 📋 일반 회의 │ │ -│ │ 기본 구조: 참석자, │ │ -│ │ 안건, 논의, 결정, Todo│ │ -│ │ [미리보기] [✓ 선택] │ │ -│ └───────────────────────┘ │ -│ │ -│ ┌───────────────────────┐ │ -│ │ 🏃 스크럼 회의 │ │ -│ │ 어제 한 일, 오늘 할 일│ │ -│ │ 이슈 │ │ -│ │ [미리보기] [ 선택] │ │ -│ └───────────────────────┘ │ -│ │ -│ ┌───────────────────────┐ │ -│ │ 🚀 프로젝트 킥오프 │ │ -│ │ 개요, 목표, 일정, │ │ -│ │ 역할, 리스크 │ │ -│ │ [미리보기] [ 선택] │ │ -│ └───────────────────────┘ │ -│ │ -│ ┌───────────────────────┐ │ -│ │ 📊 주간 회의 │ │ -│ │ 실적, 이슈, 다음 계획 │ │ -│ │ [미리보기] [ 선택] │ │ -│ └───────────────────────┘ │ -│ │ -│ [커스터마이징] │ -│ │ -└─────────────────────────────┘ -``` - -**주요 컴포넌트**: -- 상단 헤더: 뒤로가기, 타이틀, 다음 버튼 -- 템플릿 카드: 아이콘, 제목, 설명, 미리보기 버튼, 선택 버튼 -- 커스터마이징 버튼 (선택한 템플릿이 있을 때 활성화) - -**커스터마이징 모달**: -``` -┌─────────────────────────────┐ -│ 템플릿 커스터마이징 [완료]│ -├─────────────────────────────┤ -│ │ -│ 섹션 관리 │ -│ │ -│ ☰ 참석자 │ -│ ☰ 안건 │ -│ ☰ 논의 내용 │ -│ ☰ 결정 사항 │ -│ ☰ Todo │ -│ │ -│ ┌─────────────────────┐ │ -│ │ + 섹션 추가 │ │ -│ └─────────────────────┘ │ -│ │ -│ 섹션을 길게 눌러 순서를 │ -│ 변경하거나 삭제할 수 있습니다│ -│ │ -└─────────────────────────────┘ -``` - -#### 인터랙션 -1. **템플릿 선택**: - - 카드 탭: 선택 상태 토글 - - 미리보기 버튼: 템플릿 구조 모달 표시 - - 하나만 선택 가능 (라디오 버튼 방식) - -2. **미리보기**: - - 템플릿 구조 전체 표시 - - 샘플 데이터로 예시 제공 - - 닫기 버튼으로 모달 종료 - -3. **커스터마이징**: - - 커스터마이징 버튼 클릭 → 모달 표시 - - 섹션 순서 변경: 드래그 앤 드롭 또는 위/아래 버튼 - - 섹션 삭제: 스와이프 또는 삭제 아이콘 - - 섹션 추가: 입력 필드에 섹션명 입력 후 추가 - - 완료 버튼: 변경 사항 저장 - -4. **다음 단계**: - - 다음 버튼 클릭 → 05-회의진행 화면으로 이동 - - 템플릿 정보 전달 (선택한 템플릿, 커스터마이징 내용) - -#### 데이터 요구사항 -- **입력**: 템플릿 선택, 커스터마이징 정보 -- **출력**: 준비된 회의록 구조 -- **캐싱**: 템플릿 목록 (앱 시작 시 로드) - -#### 에러 처리 -| 에러 유형 | 메시지 | 액션 | -|-----------|--------|------| -| 템플릿 미선택 | 템플릿을 선택해주세요 | 선택 유도 | -| 섹션명 중복 | 이미 존재하는 섹션명입니다 | 재입력 유도 | -| 섹션 로드 실패 | 템플릿을 불러올 수 없습니다 | 새로고침 버튼 | - ---- - -### 3.5 05-회의진행 - -#### 개요 -- **목적**: 회의 진행, 실시간 회의록 작성, 차별화 기능 제공 -- **관련 유저스토리**: UFR-MEET-030 (회의시작), UFR-AI-010 (자동작성), UFR-RAG-010/020 (용어설명), UFR-COLLAB-010/020 (협업) -- **비즈니스 중요도**: 매우 높음 (핵심 차별화 기능) -- **화면 타입**: 실시간 협업 페이지 - -#### 주요 기능 -1. 음성 녹음 및 실시간 STT 변환 -2. AI 자동 회의록 작성 및 구조화 -3. 맥락 기반 용어 설명 (차별화) -4. 실시간 협업 및 동기화 -5. 충돌 해결 -6. 회의 종료 - -#### UI 구성요소 - -**Mobile (320px ~ 767px)** -``` -┌─────────────────────────────┐ -│ [←] 프로젝트 킥오프 [종료]│ -├─────────────────────────────┤ -│ │ -│ 🔴 녹음 중 [23:45] │ -│ ┌─────────────────────┐ │ -│ │ 🎵 파형 애니메이션 │ │ -│ └─────────────────────┘ │ -│ │ -│ 👥 참석자 (3/5명) │ -│ [김민준] [박서연] [이준호] │ -│ │ -├─────────────────────────────┤ -│ 📝 실시간 회의록 │ -│ │ -│ ▼ 참석자 │ -│ - 김민준 (주관자) │ -│ - 박서연 │ -│ - 이준호 │ -│ │ -│ ▼ 안건 │ -│ - 프로젝트 목표 정의 │ -│ - 일정 및 마일스톤 │ -│ │ -│ ▼ 논의 내용 │ -│ "우리는 Q1까지 MVP를 │ -│ 완성해야 합니다..." │ -│ │ -│ 💡 [RAG] 용어 설명 │ -│ "MVP는 최소 기능 제품으로│ -│ 이전 프로젝트에서는..." │ -│ │ -│ ▼ 결정 사항 │ -│ - 개발 프레임워크: React │ -│ - 배포 환경: AWS │ -│ │ -│ ▼ Todo │ -│ ☐ 요구사항 정의 @김민준 │ -│ (~ 10/25) │ -│ │ -│ [📝 수정] [💬 댓글] │ -│ │ -└─────────────────────────────┘ -``` - -**주요 컴포넌트**: -- 상단 헤더: 뒤로가기, 회의 제목, 종료 버튼 -- 녹음 상태 영역: 녹음 표시, 진행 시간, 파형 애니메이션 -- 참석자 목록: 아바타, 이름, 참석 상태 -- 회의록 섹션: 아코디언 방식, 접기/펼치기 -- 실시간 텍스트 영역: STT 변환 결과 표시 -- AI 자동 정리 영역: 구조화된 회의록 -- 용어 하이라이트: 밑줄 또는 배경색, 툴팁 -- 액션 버튼: 수정, 댓글, 첨부 - -**맥락 기반 용어 설명 툴팁** (차별화 기능): -``` -┌─────────────────────────────┐ -│ MVP (Minimum Viable Product)│ -├─────────────────────────────┤ -│ 📘 정의: │ -│ 최소 기능 제품, 핵심 기능만 │ -│ 구현하여 시장 검증 │ -│ │ -│ 🏢 이 회의에서의 의미: │ -│ "Q1까지 사용자 인증, 대시보드│ -│ 핵심 기능만 구현" │ -│ │ -│ 📂 관련 프로젝트: │ -│ - 2024 고객 포털 프로젝트 │ -│ - 2023 모바일 앱 리뉴얼 │ -│ │ -│ 📄 과거 회의록: │ -│ - 2024-09-15 기획 회의 │ -│ - 2024-08-20 킥오프 회의 │ -│ │ -│ [자세히 보기] │ -└─────────────────────────────┘ -``` - -#### 인터랙션 -1. **회의 시작**: - - 회의 시작 버튼 클릭 → 권한 확인 (마이크) - - 녹음 시작 → 파형 애니메이션 표시 - - 참석자 목록 표시 → 실시간 참석 상태 업데이트 - -2. **실시간 STT 및 AI 작성**: - - 음성 인식 → 텍스트 변환 (1초 이내) - - AI 자동 정리 → 3-5초 간격으로 회의록 업데이트 - - 화자 식별 → 발언자 표시 - - 구조화 → 템플릿 섹션에 맞춰 자동 분류 - -3. **맥락 기반 용어 설명** (차별화): - - 전문용어 자동 감지 → 하이라이트 표시 - - 용어 클릭/탭 → 툴팁 표시 - - RAG 검색 → 과거 회의록, 사내 문서에서 맥락 추출 - - 설명 표시: - - 간단한 정의 - - 이 회의에서의 의미 - - 관련 프로젝트/이슈 - - 과거 회의록 링크 - - 사내 문서 링크 - - 툴팁 외부 클릭 → 닫기 - - "자세히 보기" → 전체 화면 모달 - -4. **실시간 협업** (UFR-COLLAB-010): - - 회의록 수정 → WebSocket으로 즉시 동기화 - - 수정 영역 하이라이트 (3초간) - - 수정자 이름 표시 - - 버전 관리 → 수정 이력 저장 - -5. **충돌 해결** (UFR-COLLAB-020): - - 동일 위치 동시 수정 감지 - - 충돌 알림 표시 - - 해결 방식 선택: - - Last Write Wins (기본) - - 수동 병합 (비교 UI 표시) - -6. **회의 종료**: - - 종료 버튼 클릭 → 확인 다이얼로그 - - 확인 → 녹음 중지 → 06-검증완료 화면으로 이동 - -#### 데이터 요구사항 -- **입력**: 음성 스트림, 회의 ID, 참석자 정보 -- **출력**: 텍스트 변환 결과, 구조화된 회의록, 용어 설명 -- **실시간 동기화**: WebSocket 연결 -- **로컬 저장**: 임시 회의록 (30초 간격) - -#### 에러 처리 -| 에러 유형 | 메시지 | 액션 | -|-----------|--------|------| -| 마이크 권한 없음 | 마이크 권한이 필요합니다 | 설정 안내 | -| STT 실패 | 음성 인식에 실패했습니다 | 수동 입력 모드 | -| AI 처리 실패 | 자동 정리 중 오류 발생 | 재시도 버튼 | -| 동기화 실패 | 연결이 끊어졌습니다 | 재연결 시도 | -| 용어 설명 없음 | 관련 정보를 찾을 수 없습니다 | 전문가 요청 버튼 | - ---- - -### 3.6 06-검증완료 - -#### 개요 -- **목적**: 회의록 섹션별 검증 및 완료 표시 -- **관련 유저스토리**: UFR-COLLAB-030 -- **비즈니스 중요도**: 중간 -- **화면 타입**: 검증 및 확인 페이지 - -#### 주요 기능 -1. 섹션별 검증 상태 확인 -2. 검증 완료 체크 -3. 섹션 잠금 (회의 생성자만) -4. 다음 단계 (회의 종료) - -#### UI 구성요소 - -**Mobile (320px ~ 767px)** -``` -┌─────────────────────────────┐ -│ [←] 회의록 검증 [다음]│ -├─────────────────────────────┤ -│ │ -│ 회의록 섹션별로 검증해주세요│ -│ │ -│ ✅ 참석자 (검증완료) │ -│ ┌───────────────────────┐ │ -│ │ - 김민준 (주관자) │ │ -│ │ - 박서연 │ │ -│ │ - 이준호 │ │ -│ │ │ │ -│ │ 검증자: 김민준 │ │ -│ │ 시간: 14:35 │ │ -│ │ [수정] [🔒 잠금] │ │ -│ └───────────────────────┘ │ -│ │ -│ ⚠️ 안건 (검증 필요) │ -│ ┌───────────────────────┐ │ -│ │ - 프로젝트 목표 정의 │ │ -│ │ - 일정 및 마일스톤 │ │ -│ │ │ │ -│ │ [수정] [✓ 검증완료] │ │ -│ └───────────────────────┘ │ -│ │ -│ ⚠️ 논의 내용 (검증 필요) │ -│ ┌───────────────────────┐ │ -│ │ "우리는 Q1까지..." │ │ -│ │ │ │ -│ │ [수정] [✓ 검증완료] │ │ -│ └───────────────────────┘ │ -│ │ -│ ✅ 결정 사항 (검증완료) │ -│ ✅ Todo (검증완료) │ -│ │ -│ 전체 진행률: 60% (3/5) │ -│ │ -└─────────────────────────────┘ -``` - -**주요 컴포넌트**: -- 상단 헤더: 뒤로가기, 타이틀, 다음 버튼 -- 섹션 카드: 제목, 내용 미리보기, 검증 상태, 검증자 정보 -- 액션 버튼: 수정, 검증완료, 잠금 (회의 생성자만) -- 진행률 표시: 퍼센트, 완료 수/전체 수 - -#### 인터랙션 -1. **섹션 검증**: - - 섹션 카드 탭 → 전체 내용 표시 - - 검증완료 버튼 클릭 → 상태 업데이트 - - 검증 시간 및 검증자 기록 - - 실시간 동기화 → 다른 참석자에게 알림 - -2. **섹션 잠금** (회의 생성자만): - - 잠금 버튼 클릭 → 확인 다이얼로그 - - 확인 → 섹션 잠금 (추가 수정 불가) - - 잠금 아이콘 표시 - -3. **섹션 수정**: - - 수정 버튼 클릭 → 편집 모드 - - 인라인 편집 또는 모달 - - 저장 → 상태 "검증 필요"로 변경 - -4. **다음 단계**: - - 다음 버튼 클릭 → 07-회의종료 화면으로 이동 - - 검증 미완료 섹션이 있어도 진행 가능 (나중에 수정 가능) - -#### 데이터 요구사항 -- **입력**: 섹션별 검증 상태 -- **출력**: 검증 완료 정보 (검증자, 시간) -- **실시간 동기화**: WebSocket - -#### 에러 처리 -| 에러 유형 | 메시지 | 액션 | -|-----------|--------|------| -| 검증 권한 없음 | 검증 권한이 없습니다 | 확인 버튼 | -| 잠금 권한 없음 | 회의 생성자만 잠금할 수 있습니다 | 확인 버튼 | -| 동기화 실패 | 검증 상태 동기화 실패 | 재시도 버튼 | - ---- - -### 3.7 07-회의종료 - -#### 개요 -- **목적**: 회의 종료, 통계 확인, Todo 자동 추출, 최종 확정 -- **관련 유저스토리**: UFR-MEET-040 (회의종료), UFR-MEET-050 (최종확정), UFR-AI-020 (Todo자동추출) -- **비즈니스 중요도**: 높음 -- **화면 타입**: 요약 및 확정 페이지 - -#### 주요 기능 -1. 회의 통계 표시 -2. AI Todo 자동 추출 결과 -3. 최종 회의록 확정 -4. 회의록 공유 이동 - -#### UI 구성요소 - -**Mobile (320px ~ 767px)** -``` -┌─────────────────────────────┐ -│ [←] 회의 종료 [확정]│ -├─────────────────────────────┤ -│ │ -│ 🎉 회의가 종료되었습니다 │ -│ │ -│ 📊 회의 통계 │ -│ ┌───────────────────────┐ │ -│ │ ⏱️ 총 시간: 45분 │ │ -│ │ 👥 참석자: 3명 │ │ -│ │ 💬 발언 횟수: │ │ -│ │ 김민준 12회 │ │ -│ │ 박서연 8회 │ │ -│ │ 이준호 5회 │ │ -│ │ 🔑 주요 키워드: │ │ -│ │ #MVP #React #AWS │ │ -│ └───────────────────────┘ │ -│ │ -│ ✅ AI Todo 자동 추출 │ -│ ┌───────────────────────┐ │ -│ │ ☐ 요구사항 정의 │ │ -│ │ @김민준 ~ 10/25 │ │ -│ │ │ │ -│ │ ☐ 기술 스택 검토 │ │ -│ │ @박서연 ~ 10/27 │ │ -│ │ │ │ -│ │ ☐ 인프라 설계 │ │ -│ │ @이준호 ~ 10/30 │ │ -│ └───────────────────────┘ │ -│ │ -│ 필수 항목 확인: │ -│ ✅ 회의 제목 │ -│ ✅ 참석자 목록 │ -│ ✅ 주요 논의 내용 │ -│ ✅ 결정 사항 │ -│ │ -│ ┌─────────────────────┐ │ -│ │ 최종 회의록 확정 │ │ -│ └─────────────────────┘ │ -│ │ -│ [나중에 확정] │ -│ │ -└─────────────────────────────┘ -``` - -**주요 컴포넌트**: -- 상단 헤더: 뒤로가기, 타이틀, 확정 버튼 -- 완료 메시지: 아이콘, 텍스트 -- 통계 카드: 시간, 참석자, 발언 횟수, 키워드 -- Todo 자동 추출 영역: Todo 목록, 담당자, 마감일 -- 필수 항목 체크리스트: 체크 아이콘 -- 확정 버튼 (primary) -- 나중에 확정 버튼 (secondary) - -#### 인터랙션 -1. **회의 통계**: - - 자동 생성 및 표시 - - 그래프 또는 차트 (선택) - - 키워드 탭 → 관련 섹션으로 이동 - -2. **AI Todo 자동 추출** (UFR-AI-020): - - AI가 회의록 분석 → 액션 아이템 식별 - - 담당자 자동 지정 (발언 기반) - - 마감일 자동 추출 (언급된 경우) - - Todo 편집 가능 (담당자, 마감일 수정) - - 추가/삭제 가능 - -3. **최종 확정** (UFR-MEET-050): - - 확정 버튼 클릭 → 필수 항목 검사 - - 필수 항목 누락 시: - - 누락 항목 표시 - - 해당 섹션으로 이동 유도 - - 필수 항목 완료 시: - - 최종 버전 생성 - - 확정 시간 기록 - - Todo 서비스로 Todo 전달 (UFR-TODO-010) - - 08-회의록공유 화면으로 이동 - -4. **나중에 확정**: - - 버튼 클릭 → 대시보드로 이동 - - 회의록 상태: "작성중" - - 나중에 수정 및 확정 가능 - -#### 데이터 요구사항 -- **입력**: 회의 데이터 (시간, 참석자, 발언 내용) -- **출력**: 통계, Todo 목록, 확정 정보 -- **저장**: 최종 회의록, Todo 데이터 - -#### 에러 처리 -| 에러 유형 | 메시지 | 액션 | -|-----------|--------|------| -| 필수 항목 누락 | [항목명]이 작성되지 않았습니다 | 해당 섹션으로 이동 | -| Todo 추출 실패 | Todo 자동 추출 실패 | 수동 작성 유도 | -| 확정 실패 | 회의록 확정에 실패했습니다 | 재시도 버튼 | - ---- - -### 3.8 08-회의록공유 - -#### 개요 -- **목적**: 회의록 공유 설정 및 알림 발송 -- **관련 유저스토리**: UFR-MEET-060 -- **비즈니스 중요도**: 높음 -- **화면 타입**: 설정 및 액션 페이지 - -#### 주요 기능 -1. 공유 대상 선택 -2. 권한 설정 -3. 공유 방식 선택 -4. 링크 보안 옵션 -5. 공유 실행 및 알림 발송 - -#### UI 구성요소 - -**Mobile (320px ~ 767px)** -``` -┌─────────────────────────────┐ -│ [←] 회의록 공유 [공유]│ -├─────────────────────────────┤ -│ │ -│ 공유 대상 │ -│ ┌───────────────────────┐ │ -│ │ ● 참석자 전체 (기본) │ │ -│ │ ○ 특정 참석자 선택 │ │ -│ └───────────────────────┘ │ -│ │ -│ 공유 권한 │ -│ ┌───────────────────────┐ │ -│ │ ● 읽기 전용 │ │ -│ │ ○ 댓글 가능 │ │ -│ │ ○ 편집 가능 │ │ -│ └───────────────────────┘ │ -│ │ -│ 공유 방식 │ -│ ┌───────────────────────┐ │ -│ │ ☑️ 이메일 발송 │ │ -│ │ ☑️ 링크 복사 │ │ -│ └───────────────────────┘ │ -│ │ -│ 링크 보안 (선택) │ -│ ┌───────────────────────┐ │ -│ │ ☐ 유효 기간 설정 │ │ -│ │ [30일 ▼] │ │ -│ │ │ │ -│ │ ☐ 비밀번호 설정 │ │ -│ │ [입력 필드] │ │ -│ └───────────────────────┘ │ -│ │ -│ 🔔 다음 회의 일정 │ -│ ┌───────────────────────┐ │ -│ │ ☑️ 캘린더 자동 등록 │ │ -│ │ 날짜: [선택] │ │ -│ └───────────────────────┘ │ -│ │ -│ ┌─────────────────────┐ │ -│ │ 회의록 공유 │ │ -│ └─────────────────────┘ │ -│ │ -└─────────────────────────────┘ -``` - -**주요 컴포넌트**: -- 상단 헤더: 뒤로가기, 타이틀, 공유 버튼 -- 공유 대상 라디오 버튼 -- 권한 설정 라디오 버튼 -- 공유 방식 체크박스 -- 링크 보안 옵션: 유효 기간, 비밀번호 -- 다음 회의 일정: 캘린더 자동 등록 옵션 -- 공유 버튼 (primary) - -#### 인터랙션 -1. **공유 대상 선택**: - - 참석자 전체 (기본 선택) - - 특정 참석자 선택 → 체크박스 목록 표시 - -2. **권한 설정**: - - 읽기 전용 (기본) - - 댓글 가능 - - 편집 가능 - -3. **공유 방식**: - - 이메일 발송: 참석자 이메일로 알림 - - 링크 복사: 클립보드에 복사 → 토스트 메시지 - -4. **링크 보안**: - - 유효 기간: 드롭다운 (7일, 30일, 90일, 무제한) - - 비밀번호: 입력 필드, 자동 생성 버튼 - -5. **다음 회의 일정**: - - 회의록에서 다음 회의 언급 자동 감지 - - 캘린더 자동 등록 체크박스 - - 날짜 선택기 - -6. **공유 실행**: - - 공유 버튼 클릭 → 처리 - - 성공 시: - - 공유 링크 생성 - - 이메일 발송 (선택 시) - - 링크 복사 (선택 시) - - 캘린더 등록 (선택 시) - - 공유 시간 기록 - - 성공 메시지 표시 - - 대시보드로 이동 - - 실패 시: 오류 메시지 - -#### 데이터 요구사항 -- **입력**: 공유 대상, 권한, 방식, 보안 옵션 -- **출력**: 공유 링크, 발송 결과 -- **저장**: 공유 이력 - -#### 에러 처리 -| 에러 유형 | 메시지 | 액션 | -|-----------|--------|------| -| 공유 실패 | 공유에 실패했습니다 | 재시도 버튼 | -| 이메일 발송 실패 | 이메일 발송 실패 | 링크 복사 유도 | -| 링크 생성 실패 | 링크 생성 실패 | 재시도 버튼 | - ---- - -### 3.9 09-Todo관리 - -#### 개요 -- **목적**: Todo 할당, 진행 관리, 회의록 실시간 연동 (차별화) -- **관련 유저스토리**: UFR-TODO-010 (Todo할당), UFR-TODO-030 (Todo완료처리) -- **비즈니스 중요도**: 높음 (차별화 기능) -- **화면 타입**: 목록 및 관리 페이지 - -#### 주요 기능 -1. Todo 목록 조회 (진행중/완료) -2. Todo 완료 처리 -3. 회의록 실시간 연동 (차별화) -4. 필터링 및 정렬 -5. 알림 발송 - -#### UI 구성요소 - -**Mobile (320px ~ 767px)** -``` -┌─────────────────────────────┐ -│ [프로필] Todo [알림🔔]│ -├─────────────────────────────┤ -│ │ -│ [진행중 ▼] [마감일순 ▼] │ -│ │ -│ 📌 진행 중 (3건) │ -│ │ -│ ┌───────────────────────┐ │ -│ │ ☐ 요구사항 정의 │ │ -│ │ @김민준 │ │ -│ │ 📅 ~ 10/25 (D-5) │ │ -│ │ ⭐ 높음 │ │ -│ │ 📝 프로젝트 킥오프 │ │ -│ │ (10/20) │ │ -│ └───────────────────────┘ │ -│ │ -│ ┌───────────────────────┐ │ -│ │ ☐ 기술 스택 검토 │ │ -│ │ @박서연 │ │ -│ │ 📅 ~ 10/27 (D-7) │ │ -│ │ ⭐ 보통 │ │ -│ │ 📝 주간 회의 (10/19) │ │ -│ └───────────────────────┘ │ -│ │ -│ ✅ 완료됨 (2건) │ -│ │ -│ ┌───────────────────────┐ │ -│ │ ☑️ 회의 일정 조율 │ │ -│ │ @이준호 │ │ -│ │ ✓ 10/18 완료 │ │ -│ │ 📝 킥오프 준비 회의 │ │ -│ └───────────────────────┘ │ -│ │ -├─────────────────────────────┤ -│ [대시보드] [Todo] [더보기]│ -└─────────────────────────────┘ -``` - -**주요 컴포넌트**: -- 상단 헤더: 프로필, 타이틀, 알림 -- 필터 영역: 상태 (진행중/완료), 정렬 (마감일/우선순위/최신순) -- Todo 카드: - - 체크박스 (완료 처리) - - Todo 내용 - - 담당자 - - 마감일 (D-day 표시) - - 우선순위 (아이콘) - - 회의록 링크 (제목, 날짜) -- 하단 네비게이션 - -**Todo 상세 모달**: -``` -┌─────────────────────────────┐ -│ Todo 상세 [✕] │ -├─────────────────────────────┤ -│ │ -│ ☐ 요구사항 정의 │ -│ │ -│ 담당자: 김민준 │ -│ 마감일: 2025-10-25 │ -│ 우선순위: 높음 │ -│ │ -│ 📝 관련 회의록: │ -│ 프로젝트 킥오프 (10/20) │ -│ [회의록 보기] │ -│ │ -│ 💬 댓글 (2) │ -│ - 김민준: 진행 중입니다 │ -│ - 박서연: 도움 필요하시면 │ -│ 연락주세요 │ -│ │ -│ ┌─────────────────────┐ │ -│ │ 완료 처리 │ │ -│ └─────────────────────┘ │ -│ │ -└─────────────────────────────┘ -``` - -#### 인터랙션 -1. **Todo 목록**: - - 무한 스크롤 또는 페이지네이션 - - 카드 탭: 상세 모달 표시 - - 스와이프: 빠른 액션 (완료, 수정, 삭제) - -2. **Todo 완료 처리** (UFR-TODO-030): - - 체크박스 클릭 → 확인 다이얼로그 - - 확인 → 완료 상태 변경 - - 완료 시간 기록 - - 회의록 실시간 반영 (차별화): - - 관련 회의록의 Todo 섹션 자동 업데이트 - - 완료 표시 (체크 아이콘) - - 완료 시간 및 완료자 표시 - - 알림 발송: - - 회의 참석자에게 완료 알림 - - 모든 Todo 완료 시 전체 완료 알림 - -3. **회의록 연결** (차별화): - - 회의록 보기 버튼 → 해당 회의록 상세 화면 - - 원문 맥락 추적 가능 - - 양방향 연결 (Todo ↔ 회의록) - -4. **필터링 및 정렬**: - - 상태 필터: 진행중, 완료 - - 정렬: 마감일순, 우선순위, 최신순 - -5. **댓글 및 협업**: - - 댓글 작성 → 실시간 동기화 - - 담당자에게 알림 - -#### 데이터 요구사항 -- **입력**: 필터, 정렬, 완료 처리 -- **출력**: Todo 목록, 회의록 연결 정보 -- **실시간 동기화**: WebSocket (회의록 반영) - -#### 에러 처리 -| 에러 유형 | 메시지 | 액션 | -|-----------|--------|------| -| 조회 실패 | Todo 목록을 불러올 수 없습니다 | 새로고침 버튼 | -| 완료 처리 실패 | 완료 처리에 실패했습니다 | 재시도 버튼 | -| 회의록 연결 실패 | 회의록을 찾을 수 없습니다 | 확인 버튼 | - ---- - -## 4. 화면 간 전환 및 네비게이션 - -### 4.1 네비게이션 전략 - -**Mobile (320px ~ 767px)** -- **하단 네비게이션 바**: 대시보드, Todo, 더보기 -- **상단 헤더**: 뒤로가기, 타이틀, 액션 버튼 -- **햄버거 메뉴**: 설정, 프로필, 로그아웃 -- **플로팅 액션 버튼 (FAB)**: 빠른 액세스 (새 회의 예약) - -**Tablet (768px ~ 1023px)** -- **사이드 네비게이션**: 고정된 좌측 메뉴 -- **상단 네비게이션**: 로고, 검색, 알림, 프로필 -- **플로팅 액션 버튼 (FAB)**: 빠른 액세스 - -**Desktop (1024px 이상)** -- **좌측 사이드바**: 고정된 네비게이션 메뉴 -- **상단 헤더**: 로고, 검색, 알림, 프로필 -- **키보드 단축키**: 빠른 네비게이션 - -### 4.2 화면 전환 패턴 - -| 전환 | 애니메이션 | 방향 | -|------|------------|------| -| 로그인 → 대시보드 | 페이드인 | - | -| 대시보드 → 상세 | 슬라이드 좌 | 우 → 좌 | -| 상세 → 대시보드 | 슬라이드 우 | 좌 → 우 | -| 모달 표시 | 슬라이드 상 | 하 → 상 | -| 모달 닫기 | 슬라이드 하 | 상 → 하 | - -### 4.3 제스처 지원 (Mobile) - -- **스와이프 우**: 뒤로가기 (iOS 스타일) -- **스와이프 좌**: 빠른 액션 (삭제, 공유) -- **길게 누르기**: 컨텍스트 메뉴 -- **핀치 줌**: 이미지 확대/축소 -- **풀 투 리프레시**: 목록 새로고침 - ---- - -## 5. 반응형 설계 전략 - -### 5.1 브레이크포인트 - -```css -/* Mobile First (기본) */ -@media (min-width: 320px) { /* Mobile */ } -@media (min-width: 768px) { /* Tablet */ } -@media (min-width: 1024px) { /* Desktop */ } -@media (min-width: 1440px) { /* Large Desktop */ } -``` - -### 5.2 레이아웃 전략 - -**Mobile (320px ~ 767px)** -- 단일 컬럼 레이아웃 -- 풀 스크린 모달 -- 하단 네비게이션 -- 스택 방식 (세로 배치) - -**Tablet (768px ~ 1023px)** -- 2단 컬럼 (메인 + 사이드) -- 사이드 패널 (용어 설명, 참석자 목록) -- 플로팅 모달 -- 그리드 레이아웃 (회의록 목록 2열) - -**Desktop (1024px 이상)** -- 3단 컬럼 (네비게이션 + 메인 + 사이드) -- 인라인 모달 -- 상단 네비게이션 -- 그리드 레이아웃 (회의록 목록 3-4열) - -### 5.3 점진적 향상 - -**Mobile (기본 기능)** -- 핵심 회의록 작성 기능 -- 필수 검증 및 공유 -- 기본 Todo 관리 - -**Tablet (추가 기능)** -- 빠른 액세스 패널 -- 회의록 미리보기 -- 멀티 선택 및 일괄 작업 - -**Desktop (고급 기능)** -- 다중 회의록 비교 -- 고급 검색 및 필터 -- 키보드 단축키 -- 드래그 앤 드롭 - -### 5.4 타이포그래피 반응형 - -```css -/* 제목 */ -h1 { - font-size: 24px; /* Mobile */ -} -@media (min-width: 768px) { - h1 { font-size: 32px; } /* Tablet */ -} -@media (min-width: 1024px) { - h1 { font-size: 40px; } /* Desktop */ -} - -/* 본문 */ -body { - font-size: 14px; /* Mobile */ - line-height: 1.5; -} -@media (min-width: 768px) { - body { font-size: 16px; } /* Tablet */ -} -``` - -### 5.5 이미지 및 미디어 반응형 - -- **반응형 이미지**: `srcset`, `sizes` 속성 활용 -- **WebP 포맷**: 최신 브라우저 지원 -- **레이지 로딩**: `loading="lazy"` 속성 -- **비디오**: 자동 재생 비활성화 (모바일) - ---- - -## 6. 접근성 보장 방안 - -### 6.1 WCAG 2.1 Level AA 준수 - -#### 인식 가능성 (Perceivable) - -1. **텍스트 대체**: - - 모든 이미지에 `alt` 텍스트 제공 - - 아이콘에 `aria-label` 추가 - - 정보 전달 이미지: 상세한 설명 - - 장식 이미지: `alt=""` (빈 문자열) - -2. **색상 대비**: - - 일반 텍스트: 최소 4.5:1 - - 큰 텍스트 (18pt 이상): 최소 3:1 - - UI 컴포넌트: 최소 3:1 - - 색상만으로 정보 전달 금지 (아이콘, 텍스트 병행) - -3. **리사이징**: - - 200%까지 확대 가능 - - 콘텐츠 손실 없음 - - 가로 스크롤 최소화 - -4. **콘텐츠 구조**: - - 의미론적 HTML 사용 (`
`, `
+ + +
+ + +
+
+
+

전체 진행률

+ 60% (3/5) +
+
+
+
+
+

+ 회의록 섹션별로 검증해주세요. 모든 섹션이 검증되면 회의를 종료할 수 있습니다. +

+
+ + +
+

섹션별 검증

+ + +
+
+
+

✅ 참석자

+ 검증완료 +
+
+ 검증자: 김민준 + + 시간: 14:35 +
+
+
+

- 김민준 (주관자)

+

- 박서연

+

- 이준호

+
+ +
+ + +
+
+
+

⚠️ 안건

+ 검증 필요 +
+
+
+

- 프로젝트 목표 정의

+

- 일정 및 마일스톤

+
+ +
+ + +
+
+
+

⚠️ 논의 내용

+ 검증 필요 +
+
+
+

+ 우리는 Q1까지 MVP를 완성해야 합니다. 개발 프레임워크는 React를 사용하고, 배포 환경은 AWS로 결정했습니다. + Sprint 주기는 2주로 설정합니다. +

+
+ +
+ + +
+
+
+

✅ 결정 사항

+ 검증완료 +
+
+ 검증자: 박서연 + + 시간: 14:40 +
+
+
+

- 개발 프레임워크: React

+

- 배포 환경: AWS

+

- Sprint 주기: 2주

+
+ +
+ + +
+
+
+

✅ Todo

+ 검증완료 +
+
+ 검증자: 이준호 + + 시간: 14:42 +
+
+
+
+ +
+
요구사항 정의
+
+ @김민준 + (~ 10/25) +
+
+
+
+ +
+
기술 스택 검토
+
+ @박서연 + (~ 10/27) +
+
+
+
+ +
+ +
+ + +
+
+ 💡 + 안내 +
+

+ 검증 미완료 섹션이 있어도 다음 단계로 진행할 수 있습니다. 나중에 수정하고 다시 확정할 수 있습니다. +

+
+ +
+ + + + + + diff --git a/design/uiux/prototype/07-회의종료.html b/design/uiux/prototype/07-회의종료.html index f7095c8..fd38222 100644 --- a/design/uiux/prototype/07-회의종료.html +++ b/design/uiux/prototype/07-회의종료.html @@ -3,110 +3,470 @@ - 회의 종료 - 회의록 서비스 + + 회의 종료 - 회의록 작성 서비스 - -
-
🏁
-

회의가 종료되었습니다

-

회의록이 자동으로 저장되었습니다

+ + 본문으로 건너뛰기 - -
-
- 회의 제목 - 2025년 1분기 제품 기획 회의 + +
+
+ +

회의 종료

+ +
+
+ + +
+ + +
+
🎉
+

회의가 종료되었습니다

+

+ 회의록을 확인하고 최종 확정해주세요 +

+
+ + +
+

📊 회의 통계

+
+
+ +
+
+ ⏱️ + 총 시간 +
+
45분
+
+ + +
+
+ 👥 + 참석자 +
+
3명
+
+
+ + +
+
+ 💬 + 발언 횟수 +
+
+
+ 김민준 +
+
+
+
+ 12회 +
+
+
+ 박서연 +
+
+
+
+ 8회 +
+
+
+ 이준호 +
+
+
+
+ 5회 +
+
+
+
+ + +
+
+ 🔑 + 주요 키워드 +
+
+ #MVP + #React + #AWS + #Sprint + #Q1 +
+
-
- 회의 시간 - 45분 +
+ + +
+

✅ AI Todo 자동 추출

+
+
+ 💡 + AI가 회의록에서 3개의 Todo를 자동으로 추출했습니다 +
+ + +
+ +
+
요구사항 정의서 작성
+
+ @김민준 + 📅 ~ 10/25 + +
+
+
+ + +
+ +
+
기술 스택 상세 검토
+
+ @박서연 + 📅 ~ 10/27 + +
+
+
+ + +
+ +
+
인프라 설계 문서 작성
+
+ @이준호 + 📅 ~ 10/30 + +
+
+
+ +
+ +
-
- 참석자 - 3명 +
+ + +
+

필수 항목 확인

+
+
+
+ + 회의 제목 +
+
+ + 참석자 목록 +
+
+ + 주요 논의 내용 +
+
+ + 결정 사항 +
+
-
- 생성된 Todo - 5개 +
+ + +
+ + +
+ +
+ + + - -
- - + + diff --git a/design/uiux/prototype/08-최종확정.html b/design/uiux/prototype/08-최종확정.html deleted file mode 100644 index 0096913..0000000 --- a/design/uiux/prototype/08-최종확정.html +++ /dev/null @@ -1,303 +0,0 @@ - - - - - - 회의록 최종 확정 - 회의록 서비스 - - - - -
- - -
- ⚠️ 아래 필수 항목을 모두 확인해주세요. -
- -
- -
-

2025년 1분기 제품 기획 회의

-
-

날짜: 2025-10-25 14:00
- 장소: 본사 2층 대회의실
- 참석자: 김민준, 박서연, 이준호

- -

안건

-
    -
  • 신규 기능 개발 일정 논의
  • -
  • 예산 편성 검토
  • -
- -

논의 내용

-

신규 회의록 서비스의 핵심 기능에 대해 논의했습니다. AI 기반 자동 작성 기능과 실시간 협업 기능을 우선적으로 개발하기로 결정했습니다.

- -

개발 일정은 3월 말 완료를 목표로 하며, 주요 마일스톤은 다음과 같습니다:

-
    -
  • 3월 10일: 기본 UI 완성
  • -
  • 3월 20일: AI 기능 통합
  • -
  • 3월 30일: 베타 테스트 시작
  • -
- -

결정 사항

-
    -
  • 신규 기능 개발은 3월 말 완료 목표
  • -
  • 이준호님이 API 설계 담당
  • -
  • 예산은 5천만원으로 확정
  • -
- -

Todo

-
    -
  • API 명세서 작성 (담당: 이준호, 마감: 3월 25일)
  • -
  • UI 프로토타입 완성 (담당: 최유진, 마감: 3월 15일)
  • -
  • 예산 편성안 검토 (담당: 박서연, 마감: 3월 20일)
  • -
-
-
- - -
-

필수 항목 확인

- -
-
-
- 회의 제목
- 회의 제목이 명확하게 작성되었습니다 -
-
- -
-
-
- 참석자 목록
- 모든 참석자가 기록되었습니다 -
-
- -
-
-
- 주요 논의 내용
- 핵심 논의 내용이 포함되었습니다 -
-
- -
-
-
- 결정 사항
- 회의 중 결정된 사항이 명시되었습니다 -
-
- -
-
-
- Todo 생성
- 실행 항목이 Todo로 생성되었습니다 -
-
- -
-
-
- 전문용어 설명
- 필요한 용어에 설명이 추가되었습니다 -
-
-
-
- - -
- - -
-
- - - - - diff --git a/design-last/uiux/prototype/08-회의록공유.html b/design/uiux/prototype/08-회의록공유.html similarity index 100% rename from design-last/uiux/prototype/08-회의록공유.html rename to design/uiux/prototype/08-회의록공유.html diff --git a/design-last/uiux/prototype/09-Todo관리.html b/design/uiux/prototype/09-Todo관리.html similarity index 100% rename from design-last/uiux/prototype/09-Todo관리.html rename to design/uiux/prototype/09-Todo관리.html diff --git a/design/uiux/prototype/09-회의록공유.html b/design/uiux/prototype/09-회의록공유.html deleted file mode 100644 index c90c094..0000000 --- a/design/uiux/prototype/09-회의록공유.html +++ /dev/null @@ -1,316 +0,0 @@ - - - - - - 회의록 공유 - 회의록 서비스 - - - - -
-
🎉
-

회의록이 확정되었습니다

-

이제 참석자들과 회의록을 공유하세요

- - - - - - - - - - - - - - -
- - -
-
- - - - - diff --git a/design/uiux/prototype/10-Todo관리.html b/design/uiux/prototype/10-Todo관리.html deleted file mode 100644 index 4c3e165..0000000 --- a/design/uiux/prototype/10-Todo관리.html +++ /dev/null @@ -1,466 +0,0 @@ - - - - - - Todo 관리 - 회의록 서비스 - - - - -
- - - -
- -
-
-

시작 전

- 2 -
- -
-
데이터베이스 스키마 설계
-
-
-
- 이준호 -
-
- 📅 D-3 -
-
-
-
-
- -
- -
-
사용자 피드백 분석
-
-
-
- 박서연 -
-
- 📅 D-5 -
-
-
-
-
- -
-
- - -
-
-

진행 중

- 2 -
- -
-
API 명세서 작성
-
-
-
- 이준호 -
-
- 📅 오늘 -
-
-
-
-
- -
- -
-
예산 편성안 검토
-
-
-
- 박서연 -
-
- 📅 D+2 (지남) -
-
-
-
-
- -
-
- - -
-
-

완료

- 1 -
- -
-
UI 프로토타입 디자인
-
-
-
- 최유진 -
-
- ✅ 완료 -
-
-
-
-
- -
-
-
- - -
-
- -
-
API 명세서 작성
-
-
-
- 이준호 -
-
📅 오늘
- 진행 중 -
- -
-
- -
- -
-
예산 편성안 검토
-
-
-
- 박서연 -
-
📅 D+2 (지남)
- 진행 중 -
- -
-
- -
- -
-
UI 프로토타입 디자인
-
-
-
- 최유진 -
- 완료 -
- -
-
-
-
- - - - - diff --git a/design/uiux/prototype/11-회의록대시보드.html b/design/uiux/prototype/11-회의록대시보드.html deleted file mode 100644 index 83c5e7b..0000000 --- a/design/uiux/prototype/11-회의록대시보드.html +++ /dev/null @@ -1,1021 +0,0 @@ - - - - - - 회의록 대시보드 - 회의록 서비스 - - - - - -
-
- - ← 회의록으로 돌아가기 - -

2024 Q4 마케팅 전략 회의

-
-
- 📅 - 2024-01-15 14:00 -
-
- 📍 - 본사 대회의실 -
-
- 👥 - 참석자 5명 -
-
- -
-
- - -
- -
-
-

- 💡 - 핵심내용 -

-
-
-
-
1
-
- Q4 마케팅 예산을 전년 대비 30% 증액하여 디지털 채널 확대에 집중하기로 결정 -
-
-
-
2
-
- 신규 인플루언서 마케팅 캠페인을 2월부터 시작하며, 타겟 연령층을 20-30대로 설정 -
-
-
-
3
-
- 경쟁사 분석 결과를 바탕으로 가격 정책 재조정 필요, 가격 탄력성 분석 선행 필요 -
-
-
-
4
-
- 브랜드 리뉴얼 작업을 3월까지 완료하고, 4월에 대규모 리브랜딩 캠페인 론칭 계획 -
-
-
- -
- #디지털마케팅 - #예산증액 - #인플루언서 - #가격정책 - #브랜드리뉴얼 -
- -
-
-
5명
-
참석자
-
-
-
90분
-
회의 시간
-
-
-
32회
-
발언 횟수
-
-
-
8개
-
주요 의제
-
-
-
- - -
-
-

- - 결정사항 -

-
-
-
-
- Q4 마케팅 예산 30% 증액 승인 (총 3억 → 3.9억) -
-
-
- 👤 - 결정자: 김민준 (마케팅 본부장) -
-
- 🕐 - 14:25 -
-
-
- 배경: 디지털 채널 성과가 예상을 상회하며, 경쟁사 대비 투자 비중이 낮아 시장 점유율 확대를 위해 예산 증액 필요 -
-
- -
-
- 인플루언서 마케팅 캠페인 2월 1일 론칭 결정 -
-
-
- 👤 - 결정자: 박서연 (디지털 마케팅 팀장) -
-
- 🕐 - 14:45 -
-
-
- 배경: 인플루언서 마케팅의 ROI가 기존 채널 대비 2배 높으며, 타겟 고객층 도달률이 우수하여 조기 론칭 결정 -
-
- -
-
- 브랜드 리뉴얼 프로젝트 3월 말 완료 목표 설정 -
-
-
- 👤 - 결정자: 이준호 (브랜드 전략 팀장) -
-
- 🕐 - 15:20 -
-
-
- 배경: 현재 브랜드 이미지가 타겟 고객층과 괴리가 있으며, 4월 리브랜딩 캠페인 론칭 일정에 맞추기 위해 3월 완료 필요 -
-
-
-
- - -
-
-

- 📋 - Todo 진행상황 -

-
- -
- - - - -
- -
- -
-
-
- 👤 - 박서연 (디지털 마케팅 팀장) -
-
4개
-
-
-
-
-
인플루언서 후보 리스트 작성 및 제안서 준비
-
-
-
-
-
75%
-
-
-
-
D-5
- 높음 -
-
- -
-
-
디지털 채널별 예산 배분 계획 수립
-
-
-
-
-
40%
-
-
-
-
D-2
- 긴급 -
-
- -
-
-
SNS 채널 성과 분석 리포트 작성
-
-
-
-
-
100%
-
-
-
- 완료 -
-
- -
-
-
인플루언서 마케팅 KPI 설정
-
-
-
-
-
0%
-
-
-
-
D-10
- 보통 -
-
-
-
- - -
-
-
- 👤 - 이준호 (브랜드 전략 팀장) -
-
3개
-
-
-
-
-
브랜드 리뉴얼 디자인 시안 1차 검토
-
-
-
-
-
60%
-
-
-
-
D-7
- 높음 -
-
- -
-
-
브랜드 가이드라인 문서 업데이트
-
-
-
-
-
30%
-
-
-
-
D-14
- 보통 -
-
- -
-
-
리브랜딩 캠페인 타임라인 수립
-
-
-
-
-
0%
-
-
-
-
D-20
- 보통 -
-
-
-
- - -
-
-
- 👤 - 최유진 (가격 전략 팀장) -
-
2개
-
-
-
-
-
가격 탄력성 분석 보고서 작성
-
-
-
-
-
50%
-
-
-
-
D-1
- 긴급 -
-
- -
-
-
경쟁사 가격 정책 비교 분석
-
-
-
-
-
100%
-
-
-
- 완료 -
-
-
-
-
-
- - -
-
-

- 📚 - 참고자료 -

-
- -
-
관련 회의록 (3)
-
프로젝트 문서 (5)
-
이슈 트래커 (2)
-
위키 페이지 (4)
-
- - -
-
- - - - - - diff --git a/design-last/uiux/prototype/TEST_RESULTS.md b/design/uiux/prototype/TEST_RESULTS.md similarity index 100% rename from design-last/uiux/prototype/TEST_RESULTS.md rename to design/uiux/prototype/TEST_RESULTS.md diff --git a/design/uiux/prototype/common.css b/design/uiux/prototype/common.css index cf58303..02893db 100644 --- a/design/uiux/prototype/common.css +++ b/design/uiux/prototype/common.css @@ -2,779 +2,1292 @@ * 회의록 작성 및 공유 개선 서비스 - 공통 스타일시트 * 버전: 1.0 * 작성일: 2025-10-20 - * 레퍼런스: 스타일 가이드 v1.0 + * 설계 철학: Mobile First 디자인 + * 접근성 기준: WCAG 2.1 Level AA */ -/* ===== CSS Reset ===== */ -* { - margin: 0; - padding: 0; - box-sizing: border-box; -} - -/* ===== Root Variables (CSS Custom Properties) ===== */ +/* ==================== CSS Variables ==================== */ :root { - /* Primary Colors */ - --color-primary-light: #4DFFDB; - --color-primary-main: #00D9B1; - --color-primary-dark: #00A88A; + /* 컬러 - Primary (청록색) */ + --primary-50: #ECFDF5; + --primary-100: #D1FAE5; + --primary-200: #A7F3D0; + --primary-500: #00C896; + --primary-600: #00B589; + --primary-700: #00A07C; + --primary-900: #00725C; - /* Secondary Colors */ - --color-secondary-light: #A5B4FC; - --color-secondary-main: #6366F1; - --color-secondary-dark: #4F46E5; + /* 컬러 - Gray (회색 스케일) */ + --gray-50: #F9FAFB; + --gray-100: #F3F4F6; + --gray-200: #E5E7EB; + --gray-300: #D1D5DB; + --gray-400: #9CA3AF; + --gray-500: #6B7280; + --gray-600: #4B5563; + --gray-700: #374151; + --gray-800: #1F2937; + --gray-900: #111827; - /* Semantic Colors */ - --color-success-light: #6EE7B7; - --color-success-main: #10B981; - --color-success-dark: #059669; + /* 컬러 - Semantic (의미 색상) */ + --success-50: #ECFDF5; + --success-100: #D1FAE5; + --success-500: #10B981; + --success-700: #047857; - --color-warning-light: #FCD34D; - --color-warning-main: #F59E0B; - --color-warning-dark: #D97706; + --warning-50: #FFFBEB; + --warning-100: #FEF3C7; + --warning-500: #F59E0B; + --warning-700: #B45309; - --color-error-light: #FCA5A5; - --color-error-main: #EF4444; - --color-error-dark: #DC2626; + --error-50: #FEF2F2; + --error-100: #FEE2E2; + --error-500: #EF4444; + --error-700: #B91C1C; - --color-info-light: #93C5FD; - --color-info-main: #3B82F6; - --color-info-dark: #2563EB; + --info-50: #EFF6FF; + --info-100: #DBEAFE; + --info-500: #3B82F6; + --info-700: #1D4ED8; - /* Neutral Colors */ - --color-gray-50: #F9FAFB; - --color-gray-100: #F3F4F6; - --color-gray-200: #E5E7EB; - --color-gray-300: #D1D5DB; - --color-gray-400: #9CA3AF; - --color-gray-500: #6B7280; - --color-gray-600: #4B5563; - --color-gray-700: #374151; - --color-gray-800: #1F2937; - --color-gray-900: #111827; - --color-white: #FFFFFF; - --color-black: #000000; + /* 배경 색상 */ + --bg-primary: #FFFFFF; + --bg-secondary: #F9FAFB; + --bg-tertiary: #F3F4F6; + --bg-dark: #111827; - /* Spacing System (8px base) */ - --spacing-0: 0; - --spacing-1: 4px; - --spacing-2: 8px; - --spacing-3: 12px; - --spacing-4: 16px; - --spacing-5: 20px; - --spacing-6: 24px; - --spacing-8: 32px; - --spacing-10: 40px; - --spacing-12: 48px; - --spacing-16: 64px; - --spacing-20: 80px; + /* 텍스트 색상 */ + --text-primary: #111827; + --text-secondary: #6B7280; + --text-tertiary: #9CA3AF; + --text-disabled: #D1D5DB; + --text-inverse: #FFFFFF; - /* Font Sizes */ - --font-size-display: 48px; - --font-size-h1: 36px; - --font-size-h2: 30px; - --font-size-h3: 24px; - --font-size-h4: 20px; - --font-size-body-large: 18px; - --font-size-body: 16px; - --font-size-body-small: 14px; - --font-size-caption: 12px; + /* 폰트 패밀리 */ + --font-primary: 'Pretendard', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif; + --font-mono: 'SF Mono', 'Monaco', 'Consolas', 'Liberation Mono', monospace; - /* Font Weights */ - --font-weight-light: 300; - --font-weight-regular: 400; - --font-weight-medium: 500; - --font-weight-semibold: 600; - --font-weight-bold: 700; - - /* Line Heights */ - --line-height-tight: 1.25; - --line-height-normal: 1.5; - --line-height-relaxed: 1.75; + /* 간격 시스템 (8px 기반 그리드) */ + --space-0: 0px; + --space-1: 4px; + --space-2: 8px; + --space-3: 12px; + --space-4: 16px; + --space-5: 20px; + --space-6: 24px; + --space-8: 32px; + --space-10: 40px; + --space-12: 48px; + --space-16: 64px; /* Border Radius */ - --radius-sm: 4px; - --radius-md: 8px; - --radius-lg: 12px; - --radius-xl: 16px; - --radius-full: 50%; + --radius-none: 0px; + --radius-small: 4px; + --radius-medium: 8px; + --radius-large: 12px; + --radius-xlarge: 16px; + --radius-full: 9999px; - /* Shadows */ - --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.1); - --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.15); - --shadow-lg: 0 20px 25px rgba(0, 0, 0, 0.15); + /* Border Width */ + --border-thin: 1px; + --border-medium: 2px; + --border-thick: 4px; - /* Transitions */ - --transition-fast: 150ms ease-out; - --transition-base: 200ms ease-out; - --transition-slow: 300ms ease-out; + /* 애니메이션 지속 시간 */ + --duration-instant: 100ms; + --duration-fast: 150ms; + --duration-normal: 200ms; + --duration-slow: 300ms; + --duration-slower: 500ms; - /* Z-index */ - --z-dropdown: 1000; - --z-sticky: 1100; - --z-modal-backdrop: 1200; - --z-modal: 1300; - --z-toast: 1400; - --z-tooltip: 1500; + /* 아이콘 크기 */ + --icon-small: 16px; + --icon-medium: 20px; + --icon-large: 24px; + --icon-xlarge: 32px; } -/* ===== Typography ===== */ -body { - font-family: 'Pretendard', 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', - 'Apple SD Gothic Neo', sans-serif; - font-size: var(--font-size-body); - font-weight: var(--font-weight-regular); - line-height: var(--line-height-normal); - color: var(--color-gray-600); - background-color: var(--color-gray-50); +/* ==================== Reset CSS ==================== */ +*, *::before, *::after { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +html { + font-size: 16px; + -webkit-text-size-adjust: 100%; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } -h1, h2, h3, h4, h5, h6 { - color: var(--color-gray-900); - font-weight: var(--font-weight-bold); - line-height: var(--line-height-tight); +body { + font-family: var(--font-primary); + font-size: 0.875rem; /* 14px */ + line-height: 1.5; + color: var(--text-secondary); + background-color: var(--bg-secondary); + min-height: 100vh; } -h1 { font-size: var(--font-size-h1); letter-spacing: -0.02em; } -h2 { font-size: var(--font-size-h2); font-weight: var(--font-weight-semibold); } -h3 { font-size: var(--font-size-h3); font-weight: var(--font-weight-semibold); line-height: var(--line-height-normal); } -h4 { font-size: var(--font-size-h4); font-weight: var(--font-weight-semibold); } +img, picture, video, canvas, svg { + display: block; + max-width: 100%; +} + +input, button, textarea, select { + font: inherit; +} + +p, h1, h2, h3, h4, h5, h6 { + overflow-wrap: break-word; +} + +ul, ol { + list-style: none; +} a { - color: var(--color-primary-main); + color: inherit; text-decoration: none; - transition: color var(--transition-fast); } -a:hover { - color: var(--color-primary-dark); +button { + background: none; + border: none; + cursor: pointer; } -/* ===== Layout Utilities ===== */ -.container { - width: 100%; - padding: 0 var(--spacing-6); - margin: 0 auto; +/* ==================== Typography ==================== */ +/* Mobile First */ +h1, .h1 { + font-size: 1.5rem; /* 24px */ + font-weight: 700; + line-height: 1.25; + letter-spacing: -0.01em; + color: var(--text-primary); } +h2, .h2 { + font-size: 1.25rem; /* 20px */ + font-weight: 600; + line-height: 1.25; + color: var(--text-primary); +} + +h3, .h3 { + font-size: 1.125rem; /* 18px */ + font-weight: 600; + line-height: 1.25; + color: var(--text-primary); +} + +h4, .h4 { + font-size: 1rem; /* 16px */ + font-weight: 600; + line-height: 1.25; + color: var(--text-primary); +} + +.text-body { + font-size: 0.875rem; /* 14px */ + font-weight: 400; + line-height: 1.5; + color: var(--text-secondary); +} + +.text-caption { + font-size: 0.75rem; /* 12px */ + font-weight: 400; + line-height: 1.5; + letter-spacing: 0.02em; + color: var(--text-tertiary); +} + +.text-small { + font-size: 0.6875rem; /* 11px */ + font-weight: 400; + line-height: 1.5; + color: var(--text-tertiary); +} + +/* Tablet */ @media (min-width: 768px) { - .container { padding: 0 var(--spacing-8); } + h1, .h1 { font-size: 1.75rem; /* 28px */ } + h2, .h2 { font-size: 1.375rem; /* 22px */ } + h3, .h3 { font-size: 1.25rem; /* 20px */ } + h4, .h4 { font-size: 1.125rem; /* 18px */ } + .text-body { font-size: 1rem; /* 16px */ } + .text-caption { font-size: 0.875rem; /* 14px */ } + .text-small { font-size: 0.75rem; /* 12px */ } } +/* Desktop */ @media (min-width: 1024px) { - .container { padding: 0 var(--spacing-16); } + h1, .h1 { font-size: 2rem; /* 32px */ } + h2, .h2 { font-size: 1.5rem; /* 24px */ } } -.container-small { max-width: 640px; } -.container-medium { max-width: 768px; } -.container-large { max-width: 1024px; } -.container-xlarge { max-width: 1280px; } -.container-2xlarge { max-width: 1536px; } +/* Display (Desktop only) */ +@media (min-width: 1024px) { + .display { + font-size: 2.5rem; /* 40px */ + font-weight: 700; + line-height: 1.25; + letter-spacing: -0.01em; + color: var(--text-primary); + } +} -/* ===== Button Styles ===== */ -.btn { +@media (min-width: 1440px) { + .display { + font-size: 3rem; /* 48px */ + } +} + +/* ==================== Button Components ==================== */ +/* Base Button */ +.button { display: inline-flex; align-items: center; justify-content: center; - gap: var(--spacing-2); - padding: var(--spacing-3) var(--spacing-6); - border: none; - border-radius: var(--radius-md); - font-size: var(--font-size-body); - font-weight: var(--font-weight-medium); - line-height: 1; + gap: var(--space-2); + font-weight: 600; + border-radius: var(--radius-small); + transition: all var(--duration-instant) ease-in-out; cursor: pointer; - transition: all var(--transition-fast); - text-decoration: none; - white-space: nowrap; + min-height: 44px; + min-width: 44px; } -.btn:disabled { +.button:disabled { cursor: not-allowed; opacity: 0.6; } /* Primary Button */ -.btn-primary { - background-color: var(--color-primary-main); - color: var(--color-white); +.button-primary { + background-color: var(--primary-500); + color: var(--text-inverse); + border: none; + font-size: 0.875rem; /* 14px */ + padding: var(--space-3) var(--space-4); /* 12px 16px */ } -.btn-primary:hover:not(:disabled) { - background-color: var(--color-primary-light); - transform: translateY(-1px); - box-shadow: var(--shadow-sm); +.button-primary:hover:not(:disabled) { + background-color: var(--primary-600); } -.btn-primary:active:not(:disabled) { - background-color: var(--color-primary-dark); - transform: scale(0.98); +.button-primary:active:not(:disabled) { + background-color: var(--primary-700); } -.btn-primary:disabled { - background-color: var(--color-gray-300); - color: var(--color-gray-500); +.button-primary:disabled { + background-color: var(--gray-300); + color: var(--text-disabled); } /* Secondary Button */ -.btn-secondary { +.button-secondary { + background-color: var(--gray-50); + color: var(--text-secondary); + border: var(--border-thin) solid var(--gray-200); + font-size: 0.875rem; /* 14px */ + padding: var(--space-3) var(--space-4); /* 12px 16px */ +} + +.button-secondary:hover:not(:disabled) { + background-color: var(--gray-100); + border-color: var(--gray-300); +} + +.button-secondary:active:not(:disabled) { + background-color: var(--gray-200); +} + +/* Outline Button */ +.button-outline { background-color: transparent; - color: var(--color-primary-main); - border: 1px solid var(--color-primary-main); + color: var(--primary-500); + border: var(--border-thin) solid var(--primary-500); + font-size: 0.875rem; /* 14px */ + padding: var(--space-3) var(--space-4); /* 12px 16px */ } -.btn-secondary:hover:not(:disabled) { - background-color: rgba(0, 217, 177, 0.1); +.button-outline:hover:not(:disabled) { + background-color: var(--primary-50); } -.btn-secondary:active:not(:disabled) { - background-color: rgba(0, 217, 177, 0.2); +.button-outline:active:not(:disabled) { + background-color: var(--primary-100); } -.btn-secondary:disabled { - border-color: var(--color-gray-300); - color: var(--color-gray-400); -} - -/* Text Button */ -.btn-text { +/* Ghost Button */ +.button-ghost { background-color: transparent; - color: var(--color-gray-700); - padding: var(--spacing-2) var(--spacing-4); + color: var(--text-secondary); + border: none; + font-size: 0.875rem; /* 14px */ + padding: var(--space-3) var(--space-4); /* 12px 16px */ } -.btn-text:hover:not(:disabled) { - background-color: var(--color-gray-100); +.button-ghost:hover:not(:disabled) { + background-color: var(--gray-50); } -.btn-text:active:not(:disabled) { - background-color: var(--color-gray-200); +.button-ghost:active:not(:disabled) { + background-color: var(--gray-100); } /* Button Sizes */ -.btn-sm { padding: var(--spacing-2) var(--spacing-4); font-size: var(--font-size-body-small); } -.btn-lg { padding: var(--spacing-4) var(--spacing-8); font-size: var(--font-size-body-large); } +.button-small { + height: 32px; + padding: var(--space-2) var(--space-3); /* 8px 12px */ + font-size: 0.75rem; /* 12px */ +} + +.button-medium { + height: 40px; + padding: var(--space-3) var(--space-4); /* 12px 16px */ + font-size: 0.875rem; /* 14px */ +} + +.button-large { + height: 48px; + padding: var(--space-4) var(--space-6); /* 16px 24px */ + font-size: 1rem; /* 16px */ +} /* Icon Button */ -.btn-icon { - width: 40px; - height: 40px; - padding: 0; - border-radius: var(--radius-md); - background-color: transparent; +.button-icon { + width: 44px; + height: 44px; + padding: var(--space-3); + display: inline-flex; + align-items: center; + justify-content: center; } -.btn-icon:hover:not(:disabled) { - background-color: var(--color-gray-100); +/* ==================== Input Field ==================== */ +.input-group { + display: flex; + flex-direction: column; + gap: var(--space-2); } -.btn-icon-sm { width: 32px; height: 32px; } -.btn-icon-lg { width: 48px; height: 48px; } - -/* Floating Action Button */ -.fab { - position: fixed; - right: var(--spacing-4); - bottom: var(--spacing-4); - width: 56px; - height: 56px; - padding: 0; - border-radius: var(--radius-full); - background-color: var(--color-primary-main); - color: var(--color-white); - box-shadow: var(--shadow-md); - z-index: var(--z-sticky); -} - -.fab:hover { - box-shadow: 0 6px 16px rgba(0, 0, 0, 0.2); - transform: translateY(-2px); -} - -.fab:active { - transform: scale(0.95); -} - -/* ===== Form Styles ===== */ -.form-group { - margin-bottom: var(--spacing-4); -} - -.form-label { +.input-label { display: block; - margin-bottom: var(--spacing-2); - font-size: var(--font-size-body-small); - font-weight: var(--font-weight-medium); - color: var(--color-gray-700); + font-size: 0.875rem; /* 14px */ + font-weight: 500; + color: var(--gray-700); } -.form-input, -.form-textarea, -.form-select { +.input-label.required::after { + content: ' *'; + color: var(--error-500); +} + +.input-field { width: 100%; - padding: var(--spacing-3) var(--spacing-4); - border: 1px solid var(--color-gray-300); - border-radius: var(--radius-md); - font-size: var(--font-size-body); - font-family: inherit; - background-color: var(--color-white); - transition: all var(--transition-fast); + background-color: var(--bg-primary); + color: var(--text-primary); + border: var(--border-thin) solid var(--gray-200); + border-radius: var(--radius-small); + font-size: 0.875rem; /* 14px */ + height: 40px; + padding: 0 var(--space-4); + transition: border-color var(--duration-fast) ease-in-out; } -.form-input { - height: 48px; +.input-field:focus { + outline: none; + border-color: var(--primary-500); + box-shadow: 0 0 0 3px rgba(0, 200, 150, 0.1); } -.form-textarea { - min-height: 120px; - resize: vertical; -} - -.form-input:focus, -.form-textarea:focus, -.form-select:focus { - outline: 4px solid rgba(0, 217, 177, 0.2); - border-color: var(--color-primary-main); - border-width: 2px; -} - -.form-input::placeholder, -.form-textarea::placeholder { - color: var(--color-gray-400); -} - -.form-input:disabled, -.form-textarea:disabled, -.form-select:disabled { - background-color: var(--color-gray-100); - color: var(--color-gray-400); +.input-field:disabled { + background-color: var(--bg-secondary); + color: var(--text-disabled); cursor: not-allowed; } -.form-input.error, -.form-textarea.error, -.form-select.error { - border-color: var(--color-error-main); +.input-field.error { + border-color: var(--error-500); } -.form-error { - display: block; - margin-top: var(--spacing-1); - font-size: var(--font-size-body-small); - color: var(--color-error-main); +.input-field::placeholder { + color: var(--text-tertiary); } -/* ===== Card Styles ===== */ +.input-error-message { + font-size: 0.75rem; /* 12px */ + color: var(--error-500); + margin-top: var(--space-1); +} + +/* Textarea */ +textarea.input-field { + height: auto; + min-height: 80px; + padding: var(--space-3) var(--space-4); + resize: vertical; +} + +/* ==================== Card Component ==================== */ .card { - background-color: var(--color-white); - border: 1px solid var(--color-gray-200); - border-radius: var(--radius-lg); - padding: var(--spacing-6); - box-shadow: var(--shadow-sm); - transition: all var(--transition-base); + background-color: var(--bg-primary); + border: var(--border-thin) solid var(--gray-200); + border-radius: var(--radius-medium); + padding: var(--space-4); /* 16px - Mobile */ + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); + transition: all var(--duration-fast) ease-in-out; } -.card.interactive:hover { - box-shadow: var(--shadow-md); - transform: translateY(-2px); +@media (min-width: 768px) { + .card { + padding: var(--space-5); /* 20px - Tablet/Desktop */ + } +} + +.card:hover { + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); +} + +.card-clickable { cursor: pointer; } -.card.interactive:active { - box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); - transform: scale(0.99); +.card-clickable:hover { + border-color: var(--primary-500); } .card-header { - margin-bottom: var(--spacing-4); + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: var(--space-4); } .card-title { - font-size: var(--font-size-h4); - font-weight: var(--font-weight-semibold); - color: var(--color-gray-900); + font-size: 1.125rem; /* 18px */ + font-weight: 600; + color: var(--text-primary); } .card-body { - color: var(--color-gray-600); + color: var(--text-secondary); } -/* ===== Badge Styles ===== */ -.badge { - display: inline-flex; +.card-footer { + display: flex; align-items: center; - gap: var(--spacing-1); - padding: var(--spacing-1) var(--spacing-3); - border-radius: var(--radius-lg); - font-size: var(--font-size-caption); - font-weight: var(--font-weight-medium); - line-height: 1; + justify-content: space-between; + margin-top: var(--space-4); + padding-top: var(--space-4); + border-top: var(--border-thin) solid var(--gray-200); } -.badge-primary { - background-color: var(--color-primary-light); - color: var(--color-primary-dark); -} - -.badge-success { - background-color: var(--color-success-light); - color: var(--color-success-dark); -} - -.badge-warning { - background-color: var(--color-warning-light); - color: var(--color-warning-dark); -} - -.badge-error { - background-color: var(--color-error-light); - color: var(--color-error-dark); -} - -.badge-neutral { - background-color: var(--color-gray-200); - color: var(--color-gray-700); -} - -/* ===== Modal Styles ===== */ -.modal-backdrop { +/* ==================== Modal Component ==================== */ +.modal-overlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background-color: rgba(0, 0, 0, 0.5); - z-index: var(--z-modal-backdrop); display: flex; align-items: center; justify-content: center; - padding: var(--spacing-4); - opacity: 0; - animation: fadeIn var(--transition-base); - animation-fill-mode: forwards; + z-index: 1000; + animation: fade-in var(--duration-fast) ease-out; } .modal { - background-color: var(--color-white); - border-radius: var(--radius-xl); - padding: var(--spacing-8); - max-width: 600px; - width: 100%; + background-color: var(--bg-primary); + border-radius: var(--radius-large); + padding: var(--space-6); + width: 90%; /* Mobile */ + max-width: 480px; max-height: 90vh; overflow-y: auto; - box-shadow: var(--shadow-lg); - z-index: var(--z-modal); - opacity: 0; - transform: scale(0.95); - animation: modalIn var(--transition-base); - animation-fill-mode: forwards; + box-shadow: 0 20px 25px rgba(0, 0, 0, 0.15); + animation: slide-up var(--duration-normal) ease-out; +} + +@media (min-width: 768px) { + .modal { + width: auto; + max-width: 600px; + } +} + +@media (min-width: 1024px) { + .modal { + max-width: 800px; + } } .modal-header { - margin-bottom: var(--spacing-6); display: flex; align-items: center; justify-content: space-between; + margin-bottom: var(--space-4); } .modal-title { - font-size: var(--font-size-h3); - font-weight: var(--font-weight-semibold); - color: var(--color-gray-900); + font-size: 1.25rem; /* 20px */ + font-weight: 600; + color: var(--text-primary); } .modal-close { - padding: var(--spacing-2); background: none; border: none; + font-size: 1.5rem; /* 24px */ + color: var(--text-tertiary); cursor: pointer; - color: var(--color-gray-500); - font-size: 24px; - line-height: 1; - transition: color var(--transition-fast); + padding: 0; + width: 32px; + height: 32px; + display: flex; + align-items: center; + justify-content: center; + transition: color var(--duration-instant) ease-in-out; } .modal-close:hover { - color: var(--color-gray-700); + color: var(--text-primary); } .modal-body { - margin-bottom: var(--spacing-8); + color: var(--text-secondary); } .modal-footer { display: flex; - gap: var(--spacing-3); + gap: var(--space-3); justify-content: flex-end; + margin-top: var(--space-6); } -@keyframes fadeIn { - to { opacity: 1; } -} - -@keyframes modalIn { - to { - opacity: 1; - transform: scale(1); - } -} - -/* ===== Toast Styles ===== */ -.toast-container { - position: fixed; - top: var(--spacing-4); - right: var(--spacing-4); - z-index: var(--z-toast); - display: flex; - flex-direction: column; - gap: var(--spacing-3); -} - -.toast { - background-color: var(--color-white); - border-radius: var(--radius-md); - padding: var(--spacing-4) var(--spacing-5); - max-width: 400px; - box-shadow: var(--shadow-md); - display: flex; - align-items: flex-start; - gap: var(--spacing-3); - border-left: 4px solid var(--color-gray-400); - animation: slideInRight var(--transition-base); -} - -.toast-success { border-left-color: var(--color-success-main); } -.toast-error { border-left-color: var(--color-error-main); } -.toast-warning { border-left-color: var(--color-warning-main); } -.toast-info { border-left-color: var(--color-info-main); } - -.toast-icon { - flex-shrink: 0; - width: 20px; - height: 20px; -} - -.toast-content { - flex: 1; -} - -.toast-title { - font-weight: var(--font-weight-medium); - color: var(--color-gray-900); - margin-bottom: var(--spacing-1); -} - -.toast-message { - font-size: var(--font-size-body-small); - color: var(--color-gray-600); -} - -.toast-close { - padding: 0; - background: none; - border: none; - cursor: pointer; - color: var(--color-gray-500); - font-size: 16px; - line-height: 1; -} - -@keyframes slideInRight { - from { - transform: translateX(400px); - opacity: 0; - } - to { - transform: translateX(0); - opacity: 1; - } -} - -/* ===== Loading Styles ===== */ -.spinner { - width: 40px; - height: 40px; - border: 4px solid var(--color-gray-200); - border-top-color: var(--color-primary-main); +/* ==================== Badge Component ==================== */ +.badge { + display: inline-flex; + align-items: center; + gap: var(--space-1); + font-size: 0.75rem; /* 12px */ + font-weight: 500; + padding: var(--space-1) var(--space-2); /* 4px 8px */ border-radius: var(--radius-full); - animation: spin 1s linear infinite; -} - -.spinner-sm { width: 24px; height: 24px; border-width: 3px; } -.spinner-lg { width: 56px; height: 56px; border-width: 5px; } - -@keyframes spin { - to { transform: rotate(360deg); } -} - -.skeleton { - background-color: var(--color-gray-200); - border-radius: var(--radius-sm); - animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; -} - -@keyframes pulse { - 0%, 100% { opacity: 1; } - 50% { opacity: 0.5; } -} - -/* ===== Utility Classes ===== */ -.text-center { text-align: center; } -.text-left { text-align: left; } -.text-right { text-align: right; } - -.mt-1 { margin-top: var(--spacing-1); } -.mt-2 { margin-top: var(--spacing-2); } -.mt-3 { margin-top: var(--spacing-3); } -.mt-4 { margin-top: var(--spacing-4); } -.mt-6 { margin-top: var(--spacing-6); } -.mt-8 { margin-top: var(--spacing-8); } - -.mb-1 { margin-bottom: var(--spacing-1); } -.mb-2 { margin-bottom: var(--spacing-2); } -.mb-3 { margin-bottom: var(--spacing-3); } -.mb-4 { margin-bottom: var(--spacing-4); } -.mb-6 { margin-bottom: var(--spacing-6); } -.mb-8 { margin-bottom: var(--spacing-8); } - -.flex { display: flex; } -.flex-col { flex-direction: column; } -.items-center { align-items: center; } -.justify-center { justify-content: center; } -.justify-between { justify-content: space-between; } -.gap-2 { gap: var(--spacing-2); } -.gap-3 { gap: var(--spacing-3); } -.gap-4 { gap: var(--spacing-4); } - -.hidden { display: none; } -.block { display: block; } - -/* ===== 회의록 서비스 특화 스타일 ===== */ - -/* 상태 배지 - 회의록 전용 */ -.status-draft { - background-color: var(--color-warning-light); - color: var(--color-warning-dark); - padding: var(--spacing-1) var(--spacing-3); - border-radius: var(--radius-lg); - font-size: var(--font-size-caption); - font-weight: var(--font-weight-medium); -} - -.status-verifying { - background-color: var(--color-info-light); - color: var(--color-info-dark); - padding: var(--spacing-1) var(--spacing-3); - border-radius: var(--radius-lg); - font-size: var(--font-size-caption); - font-weight: var(--font-weight-medium); -} - -.status-confirmed { - background-color: var(--color-success-light); - color: var(--color-success-dark); - padding: var(--spacing-1) var(--spacing-3); - border-radius: var(--radius-lg); - font-size: var(--font-size-caption); - font-weight: var(--font-weight-medium); -} - -/* 전문용어 하이라이트 */ -.term-highlight { - border-bottom: 2px dotted var(--color-primary-main); - cursor: pointer; - transition: color var(--transition-fast); -} - -.term-highlight:hover { - color: var(--color-primary-dark); -} - -/* AI 제안 영역 */ -.ai-suggestion { - background-color: var(--color-gray-50); - border: 1px dashed var(--color-primary-main); - border-radius: var(--radius-md); - padding: var(--spacing-4); - margin: var(--spacing-4) 0; - position: relative; -} - -.ai-suggestion::before { - content: "✨ AI 제안"; - position: absolute; - top: -10px; - left: var(--spacing-4); - background-color: var(--color-white); - padding: 0 var(--spacing-2); - font-size: var(--font-size-caption); - font-weight: var(--font-weight-medium); - color: var(--color-primary-main); -} - -/* Todo 카드 */ -.todo-card { - background-color: var(--color-white); - border: 1px solid var(--color-gray-200); - border-radius: var(--radius-md); - padding: var(--spacing-4); - box-shadow: var(--shadow-sm); - position: relative; -} - -.todo-card.priority-high { - border-left: 4px solid var(--color-error-main); -} - -.todo-card.priority-medium { - border-left: 4px solid var(--color-warning-main); -} - -.todo-progress { - height: 4px; - background-color: var(--color-gray-200); - border-radius: 2px; - overflow: hidden; - margin-top: var(--spacing-3); -} - -.todo-progress-bar { - height: 100%; - background-color: var(--color-primary-main); - transition: width var(--transition-slow); -} - -/* 협업 커서 (예시) */ -.collab-cursor { - position: absolute; - width: 2px; - height: 20px; - pointer-events: none; - z-index: 100; -} - -.collab-cursor-label { - position: absolute; - top: -24px; - left: 0; - padding: var(--spacing-1) var(--spacing-2); - border-radius: var(--radius-sm); - font-size: var(--font-size-caption); - color: var(--color-white); white-space: nowrap; } -/* 반응형 유틸리티 */ +.badge-verified { + background-color: var(--success-50); + color: var(--success-700); +} + +.badge-pending { + background-color: var(--warning-50); + color: var(--warning-700); +} + +.badge-in-progress { + background-color: var(--info-50); + color: var(--info-700); +} + +.badge-confirmed { + background-color: var(--success-50); + color: var(--success-700); + border: var(--border-thin) solid var(--success-500); +} + +.badge-error { + background-color: var(--error-50); + color: var(--error-700); +} + +/* ==================== Progress Bar ==================== */ +.progress-bar { + width: 100%; + height: 4px; + background-color: var(--gray-200); + border-radius: var(--radius-small); + overflow: hidden; +} + +.progress-fill { + height: 100%; + background-color: var(--primary-500); + transition: width var(--duration-slow) ease-out; +} + +.progress-fill.success { + background-color: var(--success-500); +} + +.progress-fill.warning { + background-color: var(--warning-500); +} + +.progress-fill.error { + background-color: var(--error-500); +} + +/* ==================== Spinner ==================== */ +.spinner { + width: 24px; + height: 24px; + border: 3px solid var(--gray-200); + border-top-color: var(--primary-500); + border-radius: 50%; + animation: spin 0.8s linear infinite; +} + +.spinner-small { + width: 16px; + height: 16px; + border-width: 2px; +} + +.spinner-large { + width: 32px; + height: 32px; + border-width: 4px; +} + +/* ==================== Skeleton Loading ==================== */ +.skeleton { + background: linear-gradient( + 90deg, + var(--gray-50) 25%, + var(--gray-100) 50%, + var(--gray-50) 75% + ); + background-size: 200% 100%; + animation: shimmer 1.5s ease-in-out infinite; + border-radius: var(--radius-small); +} + +.skeleton-text { + height: 1rem; + margin-bottom: var(--space-2); +} + +.skeleton-title { + height: 1.5rem; + width: 60%; + margin-bottom: var(--space-3); +} + +.skeleton-avatar { + width: 40px; + height: 40px; + border-radius: 50%; +} + +/* ==================== Toast Component ==================== */ +.toast { + position: fixed; + bottom: 20px; /* Mobile */ + left: 50%; + transform: translateX(-50%); + background-color: var(--gray-900); + color: var(--text-inverse); + padding: var(--space-3) var(--space-4); /* 12px 16px */ + border-radius: var(--radius-medium); + font-size: 0.875rem; /* 14px */ + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); + z-index: 1000; + animation: toast-in var(--duration-fast) ease-out; + min-width: 280px; + max-width: 90vw; +} + +@media (min-width: 1024px) { + .toast { + bottom: auto; + top: 20px; + right: 20px; + left: auto; + transform: none; + } +} + +.toast-success { + background-color: var(--success-500); +} + +.toast-error { + background-color: var(--error-500); +} + +.toast-info { + background-color: var(--info-500); +} + +.toast-warning { + background-color: var(--warning-500); +} + +/* ==================== Todo Card Component ==================== */ +.todo-card { + background-color: var(--bg-primary); + border: var(--border-thin) solid var(--gray-200); + border-left: var(--border-thick) solid var(--gray-500); + border-radius: var(--radius-medium); + padding: var(--space-3); /* 12px */ + display: flex; + gap: var(--space-3); + align-items: flex-start; + transition: all var(--duration-fast) ease-in-out; +} + +.todo-card:hover { + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} + +.todo-card.priority-high { + border-left-color: var(--error-500); +} + +.todo-card.priority-medium { + border-left-color: var(--warning-500); +} + +.todo-card.priority-low { + border-left-color: var(--success-500); +} + +.todo-checkbox { + width: 20px; + height: 20px; + border: var(--border-medium) solid var(--gray-300); + border-radius: var(--radius-small); + cursor: pointer; + flex-shrink: 0; + transition: all var(--duration-instant) ease-in-out; +} + +.todo-checkbox.checked { + background-color: var(--success-500); + border-color: var(--success-500); + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='white'%3E%3Cpath d='M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z'/%3E%3C/svg%3E"); +} + +.todo-content { + flex: 1; +} + +.todo-title { + font-size: 0.875rem; /* 14px */ + font-weight: 500; + color: var(--text-primary); + margin-bottom: var(--space-1); +} + +.todo-title.completed { + text-decoration: line-through; + color: var(--text-tertiary); +} + +.todo-meta { + display: flex; + flex-wrap: wrap; + gap: var(--space-2); + font-size: 0.75rem; /* 12px */ + color: var(--text-secondary); +} + +.todo-assignee, +.todo-duedate { + display: inline-flex; + align-items: center; + gap: var(--space-1); +} + +.todo-duedate.urgent { + color: var(--error-500); + font-weight: 600; +} + +.todo-meeting-link { + font-size: 0.75rem; /* 12px */ + color: var(--primary-500); + text-decoration: none; + display: inline-flex; + align-items: center; + gap: var(--space-1); + margin-top: var(--space-2); +} + +.todo-meeting-link:hover { + text-decoration: underline; +} + +/* ==================== Voice Recording UI ==================== */ +.voice-recording { + background-color: var(--bg-secondary); + border: var(--border-thin) solid var(--gray-200); + border-radius: var(--radius-medium); + padding: var(--space-4); /* 16px */ + height: 80px; /* Mobile */ + display: flex; + align-items: center; + gap: var(--space-3); +} + +@media (min-width: 1024px) { + .voice-recording { + height: 100px; /* Desktop */ + } +} + +.recording-indicator { + width: 12px; + height: 12px; + background-color: var(--error-500); + border-radius: 50%; + animation: pulse 1.5s ease-in-out infinite; + flex-shrink: 0; +} + +.recording-timer { + font-size: 1.125rem; /* 18px */ + font-weight: 600; + font-family: var(--font-mono); + color: var(--text-primary); + min-width: 60px; +} + +.waveform { + flex: 1; + height: 40px; + display: flex; + align-items: center; + gap: 2px; +} + +.waveform-bar { + width: 3px; + background-color: var(--primary-500); + border-radius: 2px; + animation: wave 1s ease-in-out infinite; +} + +.waveform-bar:nth-child(1) { animation-delay: 0s; } +.waveform-bar:nth-child(2) { animation-delay: 0.1s; } +.waveform-bar:nth-child(3) { animation-delay: 0.2s; } +.waveform-bar:nth-child(4) { animation-delay: 0.3s; } +.waveform-bar:nth-child(5) { animation-delay: 0.4s; } + +/* ==================== Realtime Text Display ==================== */ +.realtime-text { + background-color: var(--bg-primary); + border: var(--border-thin) solid var(--gray-200); + border-radius: var(--radius-medium); + padding: var(--space-3); /* 12px */ + margin-bottom: var(--space-2); +} + +.speaker-name { + display: inline-block; + font-size: 0.75rem; /* 12px */ + font-weight: 600; + color: var(--primary-700); + background-color: var(--primary-50); + padding: var(--space-1) var(--space-2); /* 2px 8px */ + border-radius: var(--radius-small); + margin-bottom: var(--space-1); +} + +.timestamp { + font-size: 0.75rem; /* 12px */ + font-family: var(--font-mono); + color: var(--text-tertiary); + margin-left: var(--space-2); +} + +.text-content { + font-size: 0.875rem; /* 14px */ + line-height: 1.75; + color: var(--gray-700); +} + +.typing-indicator { + display: inline-block; + width: 4px; + height: 16px; + background-color: var(--primary-500); + animation: blink 1s step-end infinite; + margin-left: 2px; +} + +/* ==================== Term Tooltip ==================== */ +.term-highlight { + color: var(--primary-500); + border-bottom: var(--border-thin) dashed var(--primary-500); + cursor: help; + transition: color var(--duration-instant) ease-in-out; +} + +.term-highlight:hover { + color: var(--primary-700); +} + +.tooltip { + position: absolute; + background-color: var(--bg-primary); + border: var(--border-thin) solid var(--gray-200); + border-radius: var(--radius-medium); + padding: var(--space-4); /* 16px */ + width: 320px; /* Mobile */ + max-width: 90vw; + box-shadow: 0 10px 15px rgba(0, 0, 0, 0.1); + z-index: 100; + animation: fade-in var(--duration-fast) ease-out; +} + +@media (min-width: 1024px) { + .tooltip { + width: 400px; /* Desktop */ + } +} + +.tooltip::before { + content: ''; + position: absolute; + bottom: 100%; + left: 20px; + border: 8px solid transparent; + border-bottom-color: var(--gray-200); +} + +.tooltip::after { + content: ''; + position: absolute; + bottom: 100%; + left: 21px; + border: 7px solid transparent; + border-bottom-color: var(--bg-primary); +} + +.tooltip-section { + margin-bottom: var(--space-3); + padding-bottom: var(--space-3); + border-bottom: var(--border-thin) solid var(--gray-200); +} + +.tooltip-section:last-child { + margin-bottom: 0; + padding-bottom: 0; + border-bottom: none; +} + +.tooltip-title { + font-size: 0.75rem; /* 12px */ + font-weight: 600; + color: var(--text-secondary); + margin-bottom: var(--space-1); + text-transform: uppercase; + letter-spacing: 0.05em; +} + +.tooltip-content { + font-size: 0.875rem; /* 14px */ + line-height: 1.5; + color: var(--gray-700); +} + +/* ==================== Accessibility ==================== */ +/* Focus Visible */ +*:focus-visible { + outline: 2px solid var(--primary-500); + outline-offset: 2px; +} + +/* Skip to Main Content */ +.skip-to-main { + position: absolute; + top: -40px; + left: 0; + background: var(--primary-500); + color: white; + padding: var(--space-2) var(--space-4); + text-decoration: none; + z-index: 100; +} + +.skip-to-main:focus { + top: 0; +} + +/* Screen Reader Only */ +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border-width: 0; +} + +/* ==================== Animations ==================== */ +@keyframes fade-in { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +@keyframes slide-up { + from { + opacity: 0; + transform: translateY(20px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes slide-down { + from { + opacity: 0; + transform: translateY(-20px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes pulse { + 0%, 100% { + opacity: 1; + } + 50% { + opacity: 0.3; + } +} + +@keyframes spin { + to { + transform: rotate(360deg); + } +} + +@keyframes shimmer { + 0% { + background-position: 200% 0; + } + 100% { + background-position: -200% 0; + } +} + +@keyframes blink { + 0%, 100% { + opacity: 1; + } + 50% { + opacity: 0; + } +} + +@keyframes wave { + 0%, 100% { + height: 10px; + } + 50% { + height: 40px; + } +} + +@keyframes toast-in { + from { + opacity: 0; + transform: translateX(-50%) translateY(20px); + } + to { + opacity: 1; + transform: translateX(-50%) translateY(0); + } +} + +@media (min-width: 1024px) { + @keyframes toast-in { + from { + opacity: 0; + transform: translateX(20px); + } + to { + opacity: 1; + transform: translateX(0); + } + } +} + +@keyframes highlight-fade { + 0% { + background-color: var(--warning-100); + } + 100% { + background-color: transparent; + } +} + +/* ==================== Utility Classes ==================== */ +/* Margin */ +.m-0 { margin: 0; } +.m-1 { margin: var(--space-1); } +.m-2 { margin: var(--space-2); } +.m-3 { margin: var(--space-3); } +.m-4 { margin: var(--space-4); } +.m-5 { margin: var(--space-5); } +.m-6 { margin: var(--space-6); } + +.mt-0 { margin-top: 0; } +.mt-1 { margin-top: var(--space-1); } +.mt-2 { margin-top: var(--space-2); } +.mt-3 { margin-top: var(--space-3); } +.mt-4 { margin-top: var(--space-4); } +.mt-5 { margin-top: var(--space-5); } +.mt-6 { margin-top: var(--space-6); } + +.mb-0 { margin-bottom: 0; } +.mb-1 { margin-bottom: var(--space-1); } +.mb-2 { margin-bottom: var(--space-2); } +.mb-3 { margin-bottom: var(--space-3); } +.mb-4 { margin-bottom: var(--space-4); } +.mb-5 { margin-bottom: var(--space-5); } +.mb-6 { margin-bottom: var(--space-6); } + +.ml-0 { margin-left: 0; } +.ml-1 { margin-left: var(--space-1); } +.ml-2 { margin-left: var(--space-2); } +.ml-3 { margin-left: var(--space-3); } +.ml-4 { margin-left: var(--space-4); } + +.mr-0 { margin-right: 0; } +.mr-1 { margin-right: var(--space-1); } +.mr-2 { margin-right: var(--space-2); } +.mr-3 { margin-right: var(--space-3); } +.mr-4 { margin-right: var(--space-4); } + +/* Padding */ +.p-0 { padding: 0; } +.p-1 { padding: var(--space-1); } +.p-2 { padding: var(--space-2); } +.p-3 { padding: var(--space-3); } +.p-4 { padding: var(--space-4); } +.p-5 { padding: var(--space-5); } +.p-6 { padding: var(--space-6); } + +.pt-0 { padding-top: 0; } +.pt-1 { padding-top: var(--space-1); } +.pt-2 { padding-top: var(--space-2); } +.pt-3 { padding-top: var(--space-3); } +.pt-4 { padding-top: var(--space-4); } + +.pb-0 { padding-bottom: 0; } +.pb-1 { padding-bottom: var(--space-1); } +.pb-2 { padding-bottom: var(--space-2); } +.pb-3 { padding-bottom: var(--space-3); } +.pb-4 { padding-bottom: var(--space-4); } + +/* Text Alignment */ +.text-left { text-align: left; } +.text-center { text-align: center; } +.text-right { text-align: right; } + +/* Display */ +.d-none { display: none; } +.d-block { display: block; } +.d-inline { display: inline; } +.d-inline-block { display: inline-block; } +.d-flex { display: flex; } +.d-inline-flex { display: inline-flex; } + +/* Flexbox */ +.flex-row { flex-direction: row; } +.flex-column { flex-direction: column; } +.flex-wrap { flex-wrap: wrap; } +.flex-nowrap { flex-wrap: nowrap; } + +.justify-start { justify-content: flex-start; } +.justify-center { justify-content: center; } +.justify-end { justify-content: flex-end; } +.justify-between { justify-content: space-between; } +.justify-around { justify-content: space-around; } + +.align-start { align-items: flex-start; } +.align-center { align-items: center; } +.align-end { align-items: flex-end; } +.align-stretch { align-items: stretch; } + +.gap-1 { gap: var(--space-1); } +.gap-2 { gap: var(--space-2); } +.gap-3 { gap: var(--space-3); } +.gap-4 { gap: var(--space-4); } + +/* Width */ +.w-full { width: 100%; } +.w-auto { width: auto; } + +/* Colors */ +.text-primary { color: var(--text-primary); } +.text-secondary { color: var(--text-secondary); } +.text-tertiary { color: var(--text-tertiary); } +.text-inverse { color: var(--text-inverse); } + +.bg-primary { background-color: var(--bg-primary); } +.bg-secondary { background-color: var(--bg-secondary); } + +/* ==================== Responsive Container ==================== */ +.container { + width: 100%; + margin-left: auto; + margin-right: auto; + padding-left: var(--space-4); /* 16px - Mobile */ + padding-right: var(--space-4); +} + +@media (min-width: 768px) { + .container { + padding-left: var(--space-6); /* 24px - Tablet */ + padding-right: var(--space-6); + } +} + +@media (min-width: 1024px) { + .container { + padding-left: var(--space-8); /* 32px - Desktop */ + padding-right: var(--space-8); + max-width: 1440px; + } +} + +/* ==================== Responsive Utilities ==================== */ +/* Mobile Only */ @media (max-width: 767px) { .hide-mobile { display: none !important; } } -@media (min-width: 768px) and (max-width: 1023px) { +/* Tablet and Up */ +@media (min-width: 768px) { .hide-tablet { display: none !important; } + .show-mobile { display: none !important; } } +/* Desktop Only */ @media (min-width: 1024px) { .hide-desktop { display: none !important; } + .show-tablet { display: none !important; } } diff --git a/design/uiux/prototype/common.js b/design/uiux/prototype/common.js index 18d6b68..19af2b5 100644 --- a/design/uiux/prototype/common.js +++ b/design/uiux/prototype/common.js @@ -1,556 +1,1100 @@ -/* - * 회의록 작성 및 공유 개선 서비스 - 공통 자바스크립트 - * 버전: 1.0 - * 작성일: 2025-10-20 - * 레퍼런스: 스타일 가이드 v1.0 +/** + * 회의록 작성 및 공유 개선 서비스 - 공통 Javascript + * @version 1.0 + * @author 최유진 (Frontend Developer) + * @date 2025-10-20 + * + * 이 파일은 모든 HTML 프로토타입에서 공통으로 사용되는 유틸리티 함수와 + * 상태 관리, 예제 데이터를 제공합니다. */ -// ===== 전역 상태 관리 ===== -const AppState = { - currentUser: { - id: 'user-001', - name: '김민준', - email: 'minjun.kim@example.com', - avatar: 'https://ui-avatars.com/api/?name=김민준&background=00D9B1&color=fff' - }, - meetings: [], - todos: [] -}; - -// ===== 유틸리티 함수 ===== +/* ============================================================================ + 1. DOM 헬퍼 함수 + ============================================================================ */ /** - * DOM 준비 완료 시 콜백 실행 + * 단일 요소 선택 (querySelector 단축) + * @param {string} selector - CSS 선택자 + * @returns {Element|null} */ -function ready(callback) { - if (document.readyState !== 'loading') { - callback(); +const $ = (selector) => document.querySelector(selector); + +/** + * 복수 요소 선택 (querySelectorAll 단축) + * @param {string} selector - CSS 선택자 + * @returns {NodeList} + */ +const $$ = (selector) => document.querySelectorAll(selector); + +/** + * 엘리먼트 생성 헬퍼 + * @param {string} tag - HTML 태그명 + * @param {string} className - CSS 클래스명 (선택) + * @param {string} text - 텍스트 콘텐츠 (선택) + * @returns {HTMLElement} + */ +function createElement(tag, className = '', text = '') { + const element = document.createElement(tag); + if (className) element.className = className; + if (text) element.textContent = text; + return element; +} + +/** + * 클래스 추가 + * @param {Element} element - 대상 엘리먼트 + * @param {string} className - 추가할 클래스명 + */ +function addClass(element, className) { + if (element) element.classList.add(className); +} + +/** + * 클래스 제거 + * @param {Element} element - 대상 엘리먼트 + * @param {string} className - 제거할 클래스명 + */ +function removeClass(element, className) { + if (element) element.classList.remove(className); +} + +/** + * 클래스 토글 + * @param {Element} element - 대상 엘리먼트 + * @param {string} className - 토글할 클래스명 + */ +function toggleClass(element, className) { + if (element) element.classList.toggle(className); +} + +/* ============================================================================ + 2. 화면 네비게이션 유틸리티 + ============================================================================ */ + +/** + * 화면 이동 함수 + * @param {string} screenName - 이동할 화면 파일명 (예: '02-대시보드.html') + */ +function navigateTo(screenName) { + // 파일 확장자가 없으면 .html 추가 + const fileName = screenName.includes('.html') ? screenName : `${screenName}.html`; + + // 현재 경로 저장 (뒤로가기 용) + const currentPath = window.location.pathname; + sessionStorage.setItem('previousScreen', currentPath); + + // 화면 전환 애니메이션 (선택) + document.body.style.opacity = '0'; + setTimeout(() => { + window.location.href = fileName; + }, 150); +} + +/** + * 뒤로가기 함수 + */ +function goBack() { + const previousScreen = sessionStorage.getItem('previousScreen'); + + if (previousScreen && previousScreen !== window.location.pathname) { + navigateTo(previousScreen); } else { - document.addEventListener('DOMContentLoaded', callback); + // 이전 화면이 없으면 대시보드로 + navigateTo('02-대시보드.html'); + } +} + +/* ============================================================================ + 3. 모달 관리 + ============================================================================ */ + +/** + * 모달 표시 + * @param {string} id - 모달 엘리먼트 ID + */ +function showModal(id) { + const modal = $(`#${id}`); + if (!modal) { + console.error(`Modal with id "${id}" not found`); + return; + } + + modal.style.display = 'flex'; + modal.setAttribute('aria-hidden', 'false'); + + // 애니메이션 + setTimeout(() => { + addClass(modal, 'modal-active'); + }, 10); + + // 배경 스크롤 방지 + document.body.style.overflow = 'hidden'; + + // ESC 키로 닫기 + const handleEscape = (e) => { + if (e.key === 'Escape') { + hideModal(id); + document.removeEventListener('keydown', handleEscape); + } + }; + document.addEventListener('keydown', handleEscape); + + // 포커스 트랩 + trapFocus(modal); +} + +/** + * 모달 숨기기 + * @param {string} id - 모달 엘리먼트 ID + */ +function hideModal(id) { + const modal = $(`#${id}`); + if (!modal) return; + + removeClass(modal, 'modal-active'); + + setTimeout(() => { + modal.style.display = 'none'; + modal.setAttribute('aria-hidden', 'true'); + document.body.style.overflow = ''; + }, 150); +} + +/** + * 모달 외부 클릭 시 닫기 설정 + * @param {string} id - 모달 엘리먼트 ID + */ +function setupModalClickOutside(id) { + const modal = $(`#${id}`); + if (!modal) return; + + modal.addEventListener('click', (e) => { + if (e.target === modal) { + hideModal(id); + } + }); +} + +/** + * 포커스 트랩 (모달 내부에만 포커스 유지) + * @param {Element} container - 포커스를 가둘 컨테이너 + */ +function trapFocus(container) { + const focusableElements = container.querySelectorAll( + 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])' + ); + const firstElement = focusableElements[0]; + const lastElement = focusableElements[focusableElements.length - 1]; + + firstElement?.focus(); + + container.addEventListener('keydown', (e) => { + if (e.key !== 'Tab') return; + + if (e.shiftKey && document.activeElement === firstElement) { + e.preventDefault(); + lastElement?.focus(); + } else if (!e.shiftKey && document.activeElement === lastElement) { + e.preventDefault(); + firstElement?.focus(); + } + }); +} + +/* ============================================================================ + 4. Toast 알림 + ============================================================================ */ + +/** + * 토스트 메시지 표시 + * @param {string} message - 표시할 메시지 + * @param {string} type - 타입 (success|error|info|warning) + * @param {number} duration - 표시 시간 (ms, 기본 3000) + */ +function showToast(message, type = 'info', duration = 3000) { + // 기존 토스트 제거 + const existingToast = $('.toast'); + if (existingToast) { + existingToast.remove(); + } + + // 토스트 생성 + const toast = createElement('div', `toast toast-${type}`, message); + toast.setAttribute('role', 'alert'); + toast.setAttribute('aria-live', 'polite'); + + document.body.appendChild(toast); + + // 애니메이션 + setTimeout(() => { + addClass(toast, 'toast-show'); + }, 10); + + // 자동 제거 + setTimeout(() => { + removeClass(toast, 'toast-show'); + setTimeout(() => { + toast.remove(); + }, 150); + }, duration); +} + +/* ============================================================================ + 5. 폼 검증 + ============================================================================ */ + +/** + * 이메일 형식 검증 + * @param {string} email - 검증할 이메일 + * @returns {boolean} + */ +function validateEmail(email) { + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + return emailRegex.test(email); +} + +/** + * 필수 입력 검증 + * @param {string} value - 검증할 값 + * @returns {boolean} + */ +function validateRequired(value) { + return value !== null && value !== undefined && value.trim() !== ''; +} + +/** + * 폼 전체 검증 + * @param {HTMLFormElement} formElement - 검증할 폼 엘리먼트 + * @returns {boolean} - 모든 필드가 유효하면 true + */ +function validateForm(formElement) { + if (!formElement) return false; + + let isValid = true; + const inputs = formElement.querySelectorAll('input[required], textarea[required], select[required]'); + + inputs.forEach(input => { + const value = input.value; + const errorMsg = input.parentElement.querySelector('.input-error-message'); + + // 필수 입력 검증 + if (!validateRequired(value)) { + isValid = false; + addClass(input, 'error'); + + if (errorMsg) { + errorMsg.textContent = `${input.getAttribute('aria-label') || '이 항목'}을 입력해주세요`; + } + } + // 이메일 검증 + else if (input.type === 'email' && !validateEmail(value)) { + isValid = false; + addClass(input, 'error'); + + if (errorMsg) { + errorMsg.textContent = '올바른 이메일을 입력해주세요'; + } + } + // 검증 통과 + else { + removeClass(input, 'error'); + if (errorMsg) { + errorMsg.textContent = ''; + } + } + }); + + return isValid; +} + +/** + * 실시간 입력 검증 설정 + * @param {HTMLInputElement} input - 입력 필드 + */ +function setupRealtimeValidation(input) { + input.addEventListener('blur', () => { + const value = input.value; + const errorMsg = input.parentElement.querySelector('.input-error-message'); + + if (input.hasAttribute('required') && !validateRequired(value)) { + addClass(input, 'error'); + if (errorMsg) { + errorMsg.textContent = `${input.getAttribute('aria-label') || '이 항목'}을 입력해주세요`; + } + } else if (input.type === 'email' && !validateEmail(value)) { + addClass(input, 'error'); + if (errorMsg) { + errorMsg.textContent = '올바른 이메일을 입력해주세요'; + } + } else { + removeClass(input, 'error'); + if (errorMsg) { + errorMsg.textContent = ''; + } + } + }); + + input.addEventListener('input', () => { + removeClass(input, 'error'); + }); +} + +/* ============================================================================ + 6. 로컬 스토리지 관리 + ============================================================================ */ + +/** + * 데이터 저장 + * @param {string} key - 저장할 키 + * @param {any} value - 저장할 값 + */ +function saveData(key, value) { + try { + const jsonValue = JSON.stringify(value); + localStorage.setItem(key, jsonValue); + } catch (error) { + console.error('데이터 저장 실패:', error); } } /** - * 날짜 포맷팅 (YYYY-MM-DD HH:mm) + * 데이터 로드 + * @param {string} key - 로드할 키 + * @returns {any} - 저장된 값 (없으면 null) */ -function formatDateTime(date) { +function loadData(key) { + try { + const jsonValue = localStorage.getItem(key); + return jsonValue ? JSON.parse(jsonValue) : null; + } catch (error) { + console.error('데이터 로드 실패:', error); + return null; + } +} + +/** + * 데이터 삭제 + * @param {string} key - 삭제할 키 + */ +function removeData(key) { + try { + localStorage.removeItem(key); + } catch (error) { + console.error('데이터 삭제 실패:', error); + } +} + +/* ============================================================================ + 7. 날짜/시간 유틸리티 + ============================================================================ */ + +/** + * 날짜 포맷 (YYYY-MM-DD) + * @param {Date|string} date - 포맷할 날짜 + * @returns {string} + */ +function formatDate(date) { const d = new Date(date); const year = d.getFullYear(); const month = String(d.getMonth() + 1).padStart(2, '0'); const day = String(d.getDate()).padStart(2, '0'); - const hours = String(d.getHours()).padStart(2, '0'); - const minutes = String(d.getMinutes()).padStart(2, '0'); - return `${year}-${month}-${day} ${hours}:${minutes}`; + return `${year}-${month}-${day}`; } /** - * 상대 시간 표현 (방금 전, 3분 전, 2시간 전 등) + * 시간 포맷 (HH:MM) + * @param {Date|string} time - 포맷할 시간 + * @returns {string} */ -function timeAgo(date) { - const now = new Date(); - const past = new Date(date); - const diff = Math.floor((now - past) / 1000); // 초 단위 +function formatTime(time) { + const d = new Date(time); + const hours = String(d.getHours()).padStart(2, '0'); + const minutes = String(d.getMinutes()).padStart(2, '0'); + return `${hours}:${minutes}`; +} - if (diff < 60) return '방금 전'; - if (diff < 3600) return `${Math.floor(diff / 60)}분 전`; - if (diff < 86400) return `${Math.floor(diff / 3600)}시간 전`; - if (diff < 2592000) return `${Math.floor(diff / 86400)}일 전`; - if (diff < 31536000) return `${Math.floor(diff / 2592000)}개월 전`; - return `${Math.floor(diff / 31536000)}년 전`; +/** + * 날짜/시간 포맷 (YYYY-MM-DD HH:MM) + * @param {Date|string} datetime - 포맷할 날짜/시간 + * @returns {string} + */ +function formatDateTime(datetime) { + return `${formatDate(datetime)} ${formatTime(datetime)}`; } /** * D-day 계산 + * @param {Date|string} targetDate - 목표 날짜 + * @returns {string} - 예: 'D-5', 'D-day', 'D+3' */ -function getDday(targetDate) { - const now = new Date(); - now.setHours(0, 0, 0, 0); +function getDDay(targetDate) { + const today = new Date(); + today.setHours(0, 0, 0, 0); + const target = new Date(targetDate); target.setHours(0, 0, 0, 0); - const diff = Math.floor((target - now) / (1000 * 60 * 60 * 24)); - if (diff === 0) return '오늘'; - if (diff > 0) return `D-${diff}`; - return `D+${Math.abs(diff)} (지남)`; + const diffTime = target - today; + const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)); + + if (diffDays === 0) return 'D-day'; + if (diffDays > 0) return `D-${diffDays}`; + return `D+${Math.abs(diffDays)}`; } /** - * UUID 생성 (간단한 버전) + * 상대 시간 표시 (예: 5분 전, 2시간 전) + * @param {Date|string} date - 날짜 + * @returns {string} */ -function generateUUID() { - return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { - const r = Math.random() * 16 | 0; - const v = c === 'x' ? r : (r & 0x3 | 0x8); - return v.toString(16); +function getRelativeTime(date) { + const now = new Date(); + const past = new Date(date); + const diffMs = now - past; + const diffMins = Math.floor(diffMs / (1000 * 60)); + const diffHours = Math.floor(diffMs / (1000 * 60 * 60)); + const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24)); + + if (diffMins < 1) return '방금 전'; + if (diffMins < 60) return `${diffMins}분 전`; + if (diffHours < 24) return `${diffHours}시간 전`; + if (diffDays < 7) return `${diffDays}일 전`; + return formatDate(date); +} + +/* ============================================================================ + 8. 이벤트 처리 + ============================================================================ */ + +/** + * 디바운싱 (연속된 이벤트를 지연 처리) + * @param {Function} func - 실행할 함수 + * @param {number} delay - 지연 시간 (ms) + * @returns {Function} + */ +function debounce(func, delay = 300) { + let timeoutId; + return function (...args) { + clearTimeout(timeoutId); + timeoutId = setTimeout(() => func.apply(this, args), delay); + }; +} + +/** + * 쓰로틀링 (일정 시간마다 한 번만 실행) + * @param {Function} func - 실행할 함수 + * @param {number} limit - 제한 시간 (ms) + * @returns {Function} + */ +function throttle(func, limit = 100) { + let inThrottle; + return function (...args) { + if (!inThrottle) { + func.apply(this, args); + inThrottle = true; + setTimeout(() => { + inThrottle = false; + }, limit); + } + }; +} + +/* ============================================================================ + 9. 애니메이션 헬퍼 + ============================================================================ */ + +/** + * 페이드 인 애니메이션 + * @param {Element} element - 대상 엘리먼트 + * @param {number} duration - 지속 시간 (ms, 기본 150) + */ +function fadeIn(element, duration = 150) { + if (!element) return; + + element.style.opacity = '0'; + element.style.display = 'block'; + + let start = null; + function animate(timestamp) { + if (!start) start = timestamp; + const progress = timestamp - start; + + element.style.opacity = Math.min(progress / duration, 1); + + if (progress < duration) { + requestAnimationFrame(animate); + } + } + + requestAnimationFrame(animate); +} + +/** + * 페이드 아웃 애니메이션 + * @param {Element} element - 대상 엘리먼트 + * @param {number} duration - 지속 시간 (ms, 기본 150) + */ +function fadeOut(element, duration = 150) { + if (!element) return; + + let start = null; + function animate(timestamp) { + if (!start) start = timestamp; + const progress = timestamp - start; + + element.style.opacity = 1 - Math.min(progress / duration, 1); + + if (progress < duration) { + requestAnimationFrame(animate); + } else { + element.style.display = 'none'; + } + } + + requestAnimationFrame(animate); +} + +/** + * 슬라이드 업 애니메이션 + * @param {Element} element - 대상 엘리먼트 + * @param {number} duration - 지속 시간 (ms, 기본 200) + */ +function slideUp(element, duration = 200) { + if (!element) return; + + const height = element.offsetHeight; + element.style.overflow = 'hidden'; + element.style.transition = `height ${duration}ms ease-out`; + element.style.height = `${height}px`; + + setTimeout(() => { + element.style.height = '0'; + }, 10); + + setTimeout(() => { + element.style.display = 'none'; + element.style.height = ''; + element.style.overflow = ''; + element.style.transition = ''; + }, duration); +} + +/** + * 슬라이드 다운 애니메이션 + * @param {Element} element - 대상 엘리먼트 + * @param {number} duration - 지속 시간 (ms, 기본 200) + */ +function slideDown(element, duration = 200) { + if (!element) return; + + element.style.display = 'block'; + const height = element.offsetHeight; + + element.style.overflow = 'hidden'; + element.style.height = '0'; + element.style.transition = `height ${duration}ms ease-out`; + + setTimeout(() => { + element.style.height = `${height}px`; + }, 10); + + setTimeout(() => { + element.style.height = ''; + element.style.overflow = ''; + element.style.transition = ''; + }, duration); +} + +/* ============================================================================ + 10. 회의록 예제 데이터 + ============================================================================ */ + +/** + * 참석자 예제 데이터 + */ +const mockUsers = [ + { id: 1, name: '김민준', email: 'minjun.kim@company.com', role: 'Product Owner', avatar: '👨‍💼' }, + { id: 2, name: '박서연', email: 'seoyeon.park@company.com', role: 'AI Specialist', avatar: '👩‍💻' }, + { id: 3, name: '이준호', email: 'junho.lee@company.com', role: 'Backend Developer', avatar: '👨‍💻' }, + { id: 4, name: '최유진', email: 'yujin.choi@company.com', role: 'Frontend Developer', avatar: '👩‍🎨' }, + { id: 5, name: '정도현', email: 'dohyun.jung@company.com', role: 'QA Engineer', avatar: '👨‍🔬' } +]; + +/** + * 회의록 예제 데이터 + */ +const mockMeetings = [ + { + id: 1, + title: '프로젝트 킥오프', + date: '2025-10-20', + time: '14:00', + status: 'confirmed', // confirmed | in-progress | draft + attendees: [1, 2, 3, 4, 5], + location: '회의실 A', + template: 'kickoff', + progress: 100, + sections: [ + { + title: '참석자', + content: '김민준 (주관자), 박서연, 이준호, 최유진, 정도현', + verified: true, + verifiedBy: 1, + verifiedAt: '2025-10-20 14:45' + }, + { + title: '안건', + content: '- 프로젝트 목표 정의\n- 일정 및 마일스톤\n- 역할 분담', + verified: true + }, + { + title: '논의 내용', + content: 'Q1까지 MVP 완성을 목표로 설정. React 프레임워크와 AWS 인프라 사용 결정.', + verified: true + }, + { + title: '결정 사항', + content: '- 개발 프레임워크: React\n- 배포 환경: AWS\n- 주간 회의: 매주 월요일 10시', + verified: true + } + ], + todos: [ + { + id: 1, + content: '요구사항 정의서 작성', + assignee: 1, + dueDate: '2025-10-25', + priority: 'high', + status: 'pending' + }, + { + id: 2, + content: '기술 스택 상세 검토', + assignee: 2, + dueDate: '2025-10-27', + priority: 'medium', + status: 'pending' + } + ], + keywords: ['MVP', 'React', 'AWS'] + }, + { + id: 2, + title: '주간 회의', + date: '2025-10-19', + time: '10:00', + status: 'in-progress', + attendees: [1, 2, 3, 4, 5], + location: '온라인', + template: 'weekly', + progress: 60, + sections: [ + { + title: '참석자', + content: '김민준, 박서연, 이준호, 최유진, 정도현', + verified: true + }, + { + title: '주간 실적', + content: '- UI 프로토타입 완성\n- API 설계 초안 작성', + verified: false + }, + { + title: '주요 이슈', + content: '데이터베이스 스키마 변경 필요', + verified: false + } + ], + todos: [ + { + id: 3, + content: 'DB 스키마 수정', + assignee: 3, + dueDate: '2025-10-22', + priority: 'high', + status: 'pending' + } + ], + keywords: ['주간회의', 'UI', 'API'] + }, + { + id: 3, + title: '스프린트 계획 회의', + date: '2025-10-18', + time: '14:30', + status: 'confirmed', + attendees: [1, 2, 3, 4], + location: '회의실 B', + template: 'scrum', + progress: 100, + sections: [ + { + title: '참석자', + content: '김민준, 박서연, 이준호, 최유진', + verified: true + }, + { + title: '어제 한 일', + content: '각 팀원별 작업 진행 상황 공유', + verified: true + }, + { + title: '오늘 할 일', + content: '스프린트 목표 설정 및 작업 할당', + verified: true + } + ], + todos: [], + keywords: ['스프린트', '계획'] + }, + { + id: 4, + title: '기술 검토 회의', + date: '2025-10-17', + time: '15:00', + status: 'confirmed', + attendees: [2, 3, 4], + location: '온라인', + template: 'general', + progress: 100, + sections: [ + { + title: '참석자', + content: '박서연, 이준호, 최유진', + verified: true + }, + { + title: '논의 내용', + content: 'AI 모델 선택 및 RAG 시스템 구조 검토', + verified: true + } + ], + todos: [ + { + id: 4, + content: 'AI 모델 벤치마크 테스트', + assignee: 2, + dueDate: '2025-10-24', + priority: 'high', + status: 'completed', + completedAt: '2025-10-18' + } + ], + keywords: ['AI', 'RAG', '기술검토'] + }, + { + id: 5, + title: '디자인 리뷰', + date: '2025-10-16', + time: '11:00', + status: 'confirmed', + attendees: [1, 4, 5], + location: '회의실 C', + template: 'general', + progress: 100, + sections: [ + { + title: '참석자', + content: '김민준, 최유진, 정도현', + verified: true + }, + { + title: '논의 내용', + content: 'UI/UX 설계 및 프로토타입 리뷰', + verified: true + }, + { + title: '결정 사항', + content: 'Mobile First 접근 방식 채택', + verified: true + } + ], + todos: [ + { + id: 5, + content: '프로토타입 수정', + assignee: 4, + dueDate: '2025-10-20', + priority: 'medium', + status: 'completed', + completedAt: '2025-10-19' + } + ], + keywords: ['디자인', 'UI', 'UX'] + } +]; + +/** + * Todo 예제 데이터 + */ +const mockTodos = [ + { + id: 1, + content: '요구사항 정의서 작성', + assignee: 1, + assigneeName: '김민준', + dueDate: '2025-10-25', + priority: 'high', + status: 'pending', + meetingId: 1, + meetingTitle: '프로젝트 킥오프', + meetingDate: '2025-10-20' + }, + { + id: 2, + content: '기술 스택 상세 검토', + assignee: 2, + assigneeName: '박서연', + dueDate: '2025-10-27', + priority: 'medium', + status: 'pending', + meetingId: 1, + meetingTitle: '프로젝트 킥오프', + meetingDate: '2025-10-20' + }, + { + id: 3, + content: 'DB 스키마 수정', + assignee: 3, + assigneeName: '이준호', + dueDate: '2025-10-22', + priority: 'high', + status: 'pending', + meetingId: 2, + meetingTitle: '주간 회의', + meetingDate: '2025-10-19' + }, + { + id: 4, + content: 'AI 모델 벤치마크 테스트', + assignee: 2, + assigneeName: '박서연', + dueDate: '2025-10-24', + priority: 'high', + status: 'completed', + completedAt: '2025-10-18', + meetingId: 4, + meetingTitle: '기술 검토 회의', + meetingDate: '2025-10-17' + }, + { + id: 5, + content: '프로토타입 수정', + assignee: 4, + assigneeName: '최유진', + dueDate: '2025-10-20', + priority: 'medium', + status: 'completed', + completedAt: '2025-10-19', + meetingId: 5, + meetingTitle: '디자인 리뷰', + meetingDate: '2025-10-16' + } +]; + +/** + * 사용자별 Todo 가져오기 + * @param {number} userId - 사용자 ID + * @returns {Array} + */ +function getTodosByUser(userId) { + return mockTodos.filter(todo => todo.assignee === userId); +} + +/** + * 회의별 Todo 가져오기 + * @param {number} meetingId - 회의 ID + * @returns {Array} + */ +function getTodosByMeeting(meetingId) { + return mockTodos.filter(todo => todo.meetingId === meetingId); +} + +/** + * 사용자 정보 가져오기 + * @param {number} userId - 사용자 ID + * @returns {Object|null} + */ +function getUserById(userId) { + return mockUsers.find(user => user.id === userId) || null; +} + +/** + * 회의록 정보 가져오기 + * @param {number} meetingId - 회의 ID + * @returns {Object|null} + */ +function getMeetingById(meetingId) { + return mockMeetings.find(meeting => meeting.id === meetingId) || null; +} + +/* ============================================================================ + 11. 초기화 함수 + ============================================================================ */ + +/** + * 공통 이벤트 리스너 등록 및 초기화 + */ +function initCommon() { + // 페이지 로드 시 페이드인 + document.body.style.opacity = '1'; + + // 뒤로가기 버튼 이벤트 + const backButtons = $$('[data-action="back"]'); + backButtons.forEach(btn => { + btn.addEventListener('click', goBack); + }); + + // 모달 외부 클릭 이벤트 + const modals = $$('.modal-overlay'); + modals.forEach(modal => { + setupModalClickOutside(modal.id); + }); + + // 모달 닫기 버튼 + const closeButtons = $$('.modal-close'); + closeButtons.forEach(btn => { + btn.addEventListener('click', (e) => { + const modal = e.target.closest('.modal-overlay'); + if (modal) { + hideModal(modal.id); + } + }); + }); + + // 폼 실시간 검증 + const inputs = $$('input[required], input[type="email"]'); + inputs.forEach(input => { + setupRealtimeValidation(input); + }); + + // 네비게이션 링크 활성화 표시 + highlightCurrentPage(); + + // 로컬 스토리지 초기화 (첫 방문 시) + initLocalStorage(); + + console.log('Common.js initialized'); +} + +/** + * 현재 페이지 네비게이션 하이라이트 + */ +function highlightCurrentPage() { + const currentPath = window.location.pathname; + const navLinks = $$('[data-nav-link]'); + + navLinks.forEach(link => { + const href = link.getAttribute('href'); + if (currentPath.includes(href)) { + addClass(link, 'active'); + } }); } -// ===== 모달 관리 ===== -const Modal = { - /** - * 모달 열기 - */ - open(modalId) { - const modal = document.getElementById(modalId); - if (!modal) return; +/** + * 로컬 스토리지 초기화 (첫 방문 시) + */ +function initLocalStorage() { + if (!loadData('initialized')) { + // 현재 사용자 설정 (예제: 김민준) + saveData('currentUser', { id: 1, name: '김민준', email: 'minjun.kim@company.com' }); - modal.style.display = 'flex'; - document.body.style.overflow = 'hidden'; + // 회의록 데이터 + saveData('meetings', mockMeetings); - // backdrop 클릭 시 모달 닫기 - const backdrop = modal.querySelector('.modal-backdrop'); - if (backdrop) { - backdrop.addEventListener('click', (e) => { - if (e.target === backdrop) { - this.close(modalId); - } - }); - } + // Todo 데이터 + saveData('todos', mockTodos); - // 닫기 버튼 - const closeBtn = modal.querySelector('.modal-close'); - if (closeBtn) { - closeBtn.addEventListener('click', () => this.close(modalId)); - } - }, + // 초기화 완료 플래그 + saveData('initialized', true); - /** - * 모달 닫기 - */ - close(modalId) { - const modal = document.getElementById(modalId); - if (!modal) return; - - modal.style.display = 'none'; - document.body.style.overflow = 'auto'; + console.log('Local storage initialized with mock data'); } -}; - -// ===== 토스트 알림 ===== -const Toast = { - container: null, - - /** - * 토스트 컨테이너 초기화 - */ - init() { - if (!this.container) { - this.container = document.createElement('div'); - this.container.className = 'toast-container'; - document.body.appendChild(this.container); - } - }, - - /** - * 토스트 표시 - */ - show(message, type = 'info', duration = 4000) { - this.init(); - - const toast = document.createElement('div'); - toast.className = `toast toast-${type}`; - - const icons = { - success: '✓', - error: '✕', - warning: '⚠', - info: 'ℹ' - }; - - toast.innerHTML = ` -
${icons[type] || icons.info}
-
-
${message}
-
- - `; - - this.container.appendChild(toast); - - // 자동 제거 - setTimeout(() => { - if (toast.parentElement) { - toast.remove(); - } - }, duration); - }, - - success(message) { this.show(message, 'success'); }, - error(message) { this.show(message, 'error'); }, - warning(message) { this.show(message, 'warning'); }, - info(message) { this.show(message, 'info'); } -}; - -// ===== 로컬 스토리지 관리 ===== -const Storage = { - /** - * 데이터 저장 - */ - set(key, value) { - try { - localStorage.setItem(key, JSON.stringify(value)); - return true; - } catch (e) { - console.error('Storage.set error:', e); - return false; - } - }, - - /** - * 데이터 가져오기 - */ - get(key, defaultValue = null) { - try { - const item = localStorage.getItem(key); - return item ? JSON.parse(item) : defaultValue; - } catch (e) { - console.error('Storage.get error:', e); - return defaultValue; - } - }, - - /** - * 데이터 삭제 - */ - remove(key) { - try { - localStorage.removeItem(key); - return true; - } catch (e) { - console.error('Storage.remove error:', e); - return false; - } - }, - - /** - * 전체 삭제 - */ - clear() { - try { - localStorage.clear(); - return true; - } catch (e) { - console.error('Storage.clear error:', e); - return false; - } - } -}; - -// ===== API 호출 (Mock) ===== -const API = { - /** - * 지연 시뮬레이션 - */ - delay(ms) { - return new Promise(resolve => setTimeout(resolve, ms)); - }, - - /** - * GET 요청 (Mock) - */ - async get(endpoint) { - await this.delay(500); - console.log(`API GET: ${endpoint}`); - return { success: true, data: {} }; - }, - - /** - * POST 요청 (Mock) - */ - async post(endpoint, data) { - await this.delay(500); - console.log(`API POST: ${endpoint}`, data); - return { success: true, data: {} }; - }, - - /** - * PUT 요청 (Mock) - */ - async put(endpoint, data) { - await this.delay(500); - console.log(`API PUT: ${endpoint}`, data); - return { success: true, data: {} }; - }, - - /** - * DELETE 요청 (Mock) - */ - async delete(endpoint) { - await this.delay(500); - console.log(`API DELETE: ${endpoint}`); - return { success: true }; - } -}; - -// ===== 페이지 네비게이션 ===== -function navigateTo(page) { - // 실제로는 SPA 라우팅이나 페이지 이동 처리 - // 프로토타입에서는 링크 클릭으로 처리 - console.log(`Navigate to: ${page}`); - window.location.href = page; } -// ===== 폼 유효성 검사 ===== -const Validator = { - /** - * 이메일 유효성 검사 - */ - isEmail(email) { - const re = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; - return re.test(email); - }, +/** + * 현재 로그인 사용자 가져오기 + * @returns {Object|null} + */ +function getCurrentUser() { + return loadData('currentUser'); +} - /** - * 필수 입력 검사 - */ - required(value) { - return value !== null && value !== undefined && value.trim() !== ''; - }, +/* ============================================================================ + 12. DOM 로드 완료 시 초기화 + ============================================================================ */ - /** - * 최소 길이 검사 - */ - minLength(value, min) { - return value && value.length >= min; - }, +// DOM 로드 완료 시 공통 초기화 실행 +if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', initCommon); +} else { + initCommon(); +} - /** - * 최대 길이 검사 - */ - maxLength(value, max) { - return value && value.length <= max; - }, +/* ============================================================================ + 전역 스코프에 함수 노출 (HTML에서 직접 호출 가능) + ============================================================================ */ - /** - * 폼 필드 에러 표시 - */ - showError(inputElement, message) { - inputElement.classList.add('error'); +window.commonUtils = { + // DOM + $, + $$, + createElement, + addClass, + removeClass, + toggleClass, - let errorElement = inputElement.nextElementSibling; - if (!errorElement || !errorElement.classList.contains('form-error')) { - errorElement = document.createElement('span'); - errorElement.className = 'form-error'; - inputElement.parentElement.appendChild(errorElement); - } - errorElement.textContent = message; - }, - - /** - * 폼 필드 에러 제거 - */ - clearError(inputElement) { - inputElement.classList.remove('error'); - - const errorElement = inputElement.nextElementSibling; - if (errorElement && errorElement.classList.contains('form-error')) { - errorElement.remove(); - } - } -}; - -// ===== 로딩 상태 관리 ===== -const Loading = { - /** - * 로딩 표시 - */ - show(target = 'body') { - const element = typeof target === 'string' ? document.querySelector(target) : target; - if (!element) return; - - const spinner = document.createElement('div'); - spinner.className = 'spinner'; - spinner.id = 'global-spinner'; - spinner.style.position = 'fixed'; - spinner.style.top = '50%'; - spinner.style.left = '50%'; - spinner.style.transform = 'translate(-50%, -50%)'; - spinner.style.zIndex = '9999'; - - document.body.appendChild(spinner); - }, - - /** - * 로딩 숨김 - */ - hide() { - const spinner = document.getElementById('global-spinner'); - if (spinner) { - spinner.remove(); - } - } -}; - -// ===== 회의록 관련 유틸리티 ===== -const MeetingUtils = { - /** - * 회의 상태 레이블 - */ - getStatusLabel(status) { - const labels = { - 'scheduled': '예정', - 'in_progress': '진행 중', - 'ended': '종료', - 'draft': '작성 중', - 'verifying': '검증 중', - 'confirmed': '확정됨' - }; - return labels[status] || status; - }, - - /** - * 회의 상태 클래스 - */ - getStatusClass(status) { - const classes = { - 'draft': 'status-draft', - 'verifying': 'status-verifying', - 'confirmed': 'status-confirmed' - }; - return classes[status] || 'badge-neutral'; - }, - - /** - * Todo 우선순위 레이블 - */ - getPriorityLabel(priority) { - const labels = { - 'high': '높음', - 'medium': '보통', - 'low': '낮음' - }; - return labels[priority] || priority; - }, - - /** - * Todo 상태 레이블 - */ - getTodoStatusLabel(status) { - const labels = { - 'todo': '시작 전', - 'in_progress': '진행 중', - 'done': '완료' - }; - return labels[status] || status; - } -}; - -// ===== 예시 데이터 생성 ===== -const MockData = { - /** - * 샘플 회의 데이터 - */ - generateMeetings() { - return [ - { - id: 'm-001', - title: '2025년 1분기 제품 기획 회의', - date: '2025-10-25 14:00', - location: '본사 2층 대회의실', - status: 'scheduled', - attendees: ['김민준', '박서연', '이준호', '최유진'], - description: '신규 회의록 서비스 기획 논의' - }, - { - id: 'm-002', - title: '주간 스크럼 회의', - date: '2025-10-21 10:00', - location: 'Zoom', - status: 'confirmed', - attendees: ['김민준', '이준호', '최유진'], - description: '지난 주 진행 상황 공유 및 이번 주 계획' - }, - { - id: 'm-003', - title: 'AI 기능 개선 회의', - date: '2025-10-23 15:00', - location: '본사 3층 소회의실', - status: 'in_progress', - attendees: ['박서연', '이준호'], - description: 'LLM 기반 회의록 자동 작성 개선 방안' - } - ]; - }, - - /** - * 샘플 Todo 데이터 - */ - generateTodos() { - return [ - { - id: 't-001', - title: 'API 명세서 작성', - assignee: '이준호', - dueDate: '2025-10-25', - priority: 'high', - status: 'in_progress', - progress: 60, - meetingId: 'm-002' - }, - { - id: 't-002', - title: 'UI 프로토타입 디자인', - assignee: '최유진', - dueDate: '2025-10-23', - priority: 'medium', - status: 'done', - progress: 100, - meetingId: 'm-002' - }, - { - id: 't-003', - title: '데이터베이스 스키마 설계', - assignee: '이준호', - dueDate: '2025-10-28', - priority: 'high', - status: 'todo', - progress: 0, - meetingId: 'm-001' - } - ]; - } -}; - -// ===== 초기화 ===== -ready(() => { - console.log('Common.js loaded'); - - // 로컬 스토리지에서 상태 복원 - const savedMeetings = Storage.get('meetings'); - const savedTodos = Storage.get('todos'); - - if (!savedMeetings) { - AppState.meetings = MockData.generateMeetings(); - Storage.set('meetings', AppState.meetings); - } else { - AppState.meetings = savedMeetings; - } - - if (!savedTodos) { - AppState.todos = MockData.generateTodos(); - Storage.set('todos', AppState.todos); - } else { - AppState.todos = savedTodos; - } - - console.log('AppState initialized:', AppState); -}); - -// ===== Export (전역 네임스페이스) ===== -window.MeetingApp = { - AppState, - Modal, - Toast, - Storage, - API, - Validator, - Loading, - MeetingUtils, - MockData, + // Navigation navigateTo, + goBack, + + // Modal + showModal, + hideModal, + setupModalClickOutside, + + // Toast + showToast, + + // Validation + validateEmail, + validateRequired, + validateForm, + setupRealtimeValidation, + + // Storage + saveData, + loadData, + removeData, + + // Date/Time + formatDate, + formatTime, formatDateTime, - timeAgo, - getDday, - generateUUID, - ready + getDDay, + getRelativeTime, + + // Events + debounce, + throttle, + + // Animation + fadeIn, + fadeOut, + slideUp, + slideDown, + + // Mock Data + mockUsers, + mockMeetings, + mockTodos, + getTodosByUser, + getTodosByMeeting, + getUserById, + getMeetingById, + getCurrentUser, + + // Init + initCommon }; diff --git a/design/uiux/prototype/test-report.md b/design/uiux/prototype/test-report.md deleted file mode 100644 index f93830f..0000000 --- a/design/uiux/prototype/test-report.md +++ /dev/null @@ -1,212 +0,0 @@ -# 프로토타입 테스트 결과 보고서 - -## 테스트 일시 -2025-10-20 - -## 테스트 범위 -전체 화면 플로우 테스트 (01-로그인 ~ 10-Todo관리) - -## 테스트 결과 요약 -- 총 10개 화면 테스트 -- 정상 작동: 7개 화면 -- 버그 발견: 3개 - ---- - -## 발견된 버그 - -### 1. [HIGH] 대시보드 FAB 버튼 클릭 이벤트 미작동 -- **파일**: `02-대시보드.html` -- **위치**: 라인 682 (JavaScript) -- **증상**: FAB 버튼 클릭 시 회의 예약 페이지로 이동하지 않음 -- **원인**: JavaScript 이벤트 리스너가 제대로 바인딩되지 않음 -- **영향도**: 높음 (주요 네비게이션 기능) -- **상태**: 미수정 - -### 2. [HIGH] 회의 예약 폼 제출 버그 -- **파일**: `03-회의예약.html` -- **위치**: 라인 99 (form submit handler) -- **증상**: 필수 필드를 모두 입력해도 폼 제출 시 페이지 이동하지 않음 -- **원인**: 폼 검증 로직 또는 이벤트 핸들러 문제 -- **영향도**: 높음 (핵심 기능) -- **상태**: 미수정 - -### 3. [CRITICAL] 최종확정 페이지 링크 오류 -- **파일**: `08-최종확정.html` -- **위치**: 라인 294 -- **증상**: 회의록 확정 후 "08-회의록공유.html"로 이동 시도하여 404 오류 발생 -- **원인**: 파일명 재정렬 후 링크 업데이트 누락 -- **수정 내용**: `'08-회의록공유.html'` → `'09-회의록공유.html'` -- **영향도**: 매우 높음 (페이지 이동 불가) -- **상태**: ✅ 수정 완료 - -### 4. [LOW] common.js 중복 로드 경고 -- **파일**: 모든 HTML 파일 -- **증상**: 콘솔에 "AppState가 이미 선언되었다"는 경고 메시지 -- **원인**: 페이지 전환 시 common.js가 중복으로 로드됨 -- **영향도**: 낮음 (기능에는 영향 없음) -- **상태**: 미수정 - ---- - -## 정상 작동 화면 - -### ✅ 01-로그인.html -- 로그인 폼 정상 작동 -- 인증 성공 시 대시보드로 정상 이동 -- Toast 메시지 정상 표시 - -### ✅ 04-템플릿선택.html -- 템플릿 카드 선택 기능 정상 -- 선택 시 체크마크 표시 정상 -- "회의 시작하기" 버튼 활성화/비활성화 정상 - -### ✅ 05-회의진행.html -- 회의 에디터 정상 표시 -- 녹음 타이머 정상 작동 -- 참석자/AI 제안 탭 전환 정상 - -### ✅ 06-검증완료.html -- AI 검증 결과 정상 표시 -- 통계 카드 정상 렌더링 -- 발언 분포 그래프 정상 - -### ✅ 07-회의종료.html -- 회의 요약 정보 정상 표시 -- 버튼 네비게이션 정상 - -### ✅ 08-최종확정.html -- 회의록 미리보기 정상 표시 -- 필수 항목 체크리스트 기능 정상 -- 모든 항목 체크 시 확정 버튼 활성화 정상 - -### ✅ 09-회의록공유.html -- 공유 링크 표시 정상 -- 공유 방식 선택 UI 정상 -- 참석자 목록 표시 정상 - -### ✅ 10-Todo관리.html -- 칸반 보드 레이아웃 정상 -- Todo 카드 표시 정상 -- 담당자 아바타 표시 정상 - ---- - -## 스크린샷 -테스트 중 캡처한 스크린샷은 `.playwright-mcp/screenshots/` 디렉토리에 저장됨: -- 01-login.png -- 02-dashboard.png -- 03-meeting-reserve.png -- 03-form-filled.png -- 04-template-selection.png -- 05-meeting-progress.png -- 06-verification-complete.png -- 07-meeting-end.png -- 08-final-confirmation.png -- 09-meeting-share.png -- 10-todo-management.png - ---- - -## 다음 작업 -1. 대시보드 FAB 버튼 이벤트 핸들러 수정 -2. 회의 예약 폼 제출 로직 수정 -3. common.js 중복 로드 문제 해결 (선택적) -4. 전체 재테스트 - ---- - -## 테스트 환경 -- 브라우저: Playwright (Chromium) -- 운영체제: Windows 10 -- 테스트 도구: Claude Code + Playwright MCP - ---- - -## 개선 사항 (2025-10-20 추가) - -### Todo-회의록 자동 링크 기능 개선 - -**문제점**: 회의록과 업무이력(Todo)의 자동 링크가 명확하게 표현되지 않음 -- Todo 관리 화면에서 어떤 회의에서 생성되었는지 알 수 없음 -- 회의록 공유 화면에서 생성된 Todo 목록과 진행 상황이 표시되지 않음 -- 양방향 연결이 누락됨 - -**개선 내용**: - -1. **10-Todo관리.html 개선** - - 모든 Todo 카드에 "출처 회의록" 정보 추가 - - 회의 제목, 날짜 표시 - - 회의록으로 이동하는 클릭 가능한 링크 추가 - - 칸반 보드 5개, 리스트 뷰 3개 항목 모두 적용 - -2. **09-회의록공유.html 개선** - - "생성된 Todo" 섹션 추가 - - 3개 Todo 항목 표시 (제목, 담당자, 마감일) - - 진행 상황 표시 (진행중 60%, 완료 100%, 지연 30%) - - "Todo 보기" 링크로 Todo 관리 페이지 연결 - -**개선 효과**: -- ✅ Todo와 회의록 간 양방향 연결 구현 -- ✅ 업무 이력 추적 가능성 향상 -- ✅ 유저스토리 차별화 포인트 명확하게 구현 - - UFR-TODO-010: "관련 회의록 링크 (섹션 위치 포함)" - - UFR-RAG-020: "과거 회의록 및 업무 이력 연결" -- ✅ 회의 결과물의 실행 상황 실시간 파악 가능 - -**변경된 파일**: -- design/uiux/prototype/10-Todo관리.html (8개 위치 수정) -- design/uiux/prototype/09-회의록공유.html (1개 섹션 추가) - ---- - -### 회의 진행 중 관련 자료 실시간 제공 기능 추가 - -**문제점**: 회의 진행 중 현재 논의 주제와 관련된 과거 회의록 및 업무이력 정보 부재 -- 참석자가 이전 논의 맥락을 알 수 없음 -- 관련 Todo 진행 상황을 실시간으로 파악할 수 없음 -- 중복 논의 또는 누락된 사항 발생 가능 - -**개선 내용**: - -1. **05-회의진행.html 사이드 패널 개선** - - "관련 자료" 탭 신규 추가 (참석자, AI 제안 탭에 이어 3번째 탭) - - 실시간 컨텍스트 기반 정보 제공 - -2. **관련 회의록 섹션 (3건 표시)** - - 회의 제목, 날짜, 관련도 점수 표시 - - 회의 요약 미리보기 - - 공통 키워드 하이라이트 - - 클릭 시 새 탭에서 회의록 열기 - - 예시: - - "2024년 4분기 제품 기획 회의" (관련도 92%) - - "API 설계 리뷰 회의" (관련도 78%) - - "주간 진행 상황 점검" (관련도 71%) - -3. **관련 업무이력 섹션 (2건 표시)** - - Todo 제목, 담당자, 마감일, 진행률 표시 - - 실시간 상태 배지 (진행중/지연/완료) - - 출처 회의록 정보 표시 - - 관련 사유 설명 - - 클릭 시 Todo 관리 페이지로 이동 - - 예시: - - "API 명세서 작성" (담당: 이준호, 진행중 60%) - - "예산 편성안 검토" (담당: 박서연, 지연 30%) - -**개선 효과**: -- ✅ 회의 중 과거 맥락 실시간 파악 가능 -- ✅ 중복 논의 방지 및 연속성 확보 -- ✅ 관련 Todo 진행 상황 즉시 확인 가능 -- ✅ 유저스토리 차별화 포인트 명확하게 구현 - - UFR-AI-040: "관련 회의록 자동 연결" 구현 - - UFR-RAG-020: "관련 회의록과 업무 이력을 바탕으로 실용적인 정보 제공" 구현 - - UFR-RAG-030: "관련 문서 자동 연결" 구현 -- ✅ AI 기반 지능형 회의 진행 지원 - -**기술적 구현**: -- RAG(Retrieval-Augmented Generation) 시스템 시뮬레이션 -- 관련도 점수 알고리즘 (벡터 유사도 기반) -- 실시간 컨텍스트 분석 및 추천 - -**변경된 파일**: -- design/uiux/prototype/05-회의진행.html (1개 탭 추가, 관련 자료 섹션 구현) diff --git a/design-last/uiux/style-guide.md b/design/uiux/style-guide.md similarity index 100% rename from design-last/uiux/style-guide.md rename to design/uiux/style-guide.md diff --git a/design/uiux/uiux.md b/design/uiux/uiux.md index c37a38b..2fb0138 100644 --- a/design/uiux/uiux.md +++ b/design/uiux/uiux.md @@ -1,662 +1,1558 @@ -# 회의록 서비스 UI/UX 설계서 +# 회의록 작성 및 공유 개선 서비스 - UI/UX 설계서 + +## 문서 정보 +- **작성일**: 2025-10-20 +- **작성자**: 이미준 (서비스 기획자) +- **버전**: 1.0 +- **설계 원칙**: Mobile First 디자인 철학 +- **접근성 기준**: WCAG 2.1 Level AA + +--- ## 목차 -1. [개요](#개요) -2. [설계 원칙](#설계-원칙) -3. [회의록별 대시보드](#회의록별-대시보드) -4. [반응형 디자인 전략](#반응형-디자인-전략) -5. [접근성 가이드라인](#접근성-가이드라인) +1. [프로토타입 화면 목록](#1-프로토타입-화면-목록) +2. [화면 간 사용자 플로우](#2-화면-간-사용자-플로우) +3. [화면별 상세 설계](#3-화면별-상세-설계) +4. [화면 간 전환 및 네비게이션](#4-화면-간-전환-및-네비게이션) +5. [반응형 설계 전략](#5-반응형-설계-전략) +6. [접근성 보장 방안](#6-접근성-보장-방안) +7. [성능 최적화 방안](#7-성능-최적화-방안) +8. [변경 이력](#8-변경-이력) --- -## 개요 +## 1. 프로토타입 화면 목록 -### 프로젝트 정보 -- **프로젝트명**: 회의록 작성 및 공유 개선 서비스 -- **버전**: v1.0 -- **작성일**: 2024-01-15 -- **작성자**: 이미준 (서비스 기획자) +본 서비스는 Mobile First 설계 원칙에 따라 총 9개의 핵심 화면으로 구성됩니다. -### 설계 목적 -- 업무 지식이 없어도 누구나 쉽게 회의록을 작성하고 공유할 수 있는 서비스 -- AI 기반 자동 회의록 작성 및 Todo 추출 기능 제공 -- 관련 회의록 및 업무 이력과의 연결을 통한 맥락 기반 정보 제공 - -### 주요 차별화 포인트 -1. **맥락 기반 용어 설명**: 관련 회의록과 업무이력을 바탕으로 실용적인 정보 제공 -2. **강화된 Todo 연결**: Action item이 담당자의 Todo와 실시간으로 연결 -3. **프롬프팅 기반 회의록 개선**: AI를 활용한 다양한 형식의 회의록 생성 -4. **회의록별 대시보드**: 회의 결과를 한눈에 파악할 수 있는 통합 뷰 +| 번호 | 화면명 | 관련 유저스토리 | 비즈니스 중요도 | 비고 | +|------|--------|-----------------|-----------------|------| +| 01 | 로그인 | UFR-USER-010 | 필수 | 인증 진입점 | +| 02 | 대시보드 | UFR-MEET-045, UFR-MEET-055 | 높음 | 메인 화면 | +| 03 | 회의예약 | UFR-MEET-010 | 높음 | 회의 준비 | +| 04 | 템플릿선택 | UFR-MEET-020 | 중간 | 회의록 템플릿 | +| 05 | 회의진행 | UFR-MEET-030, UFR-AI-010, UFR-RAG-010/020, UFR-COLLAB-010/020 | 매우 높음 | 핵심 차별화 기능 | +| 06 | 검증완료 | UFR-COLLAB-030 | 중간 | 품질 보장 | +| 07 | 회의종료 | UFR-MEET-040, UFR-MEET-050, UFR-AI-020 | 높음 | 회의록 확정 | +| 08 | 회의록공유 | UFR-MEET-060 | 높음 | 공유 및 협업 | +| 09 | Todo관리 | UFR-TODO-010, UFR-TODO-030 | 높음 | 차별화 기능 | --- -## 설계 원칙 +## 2. 화면 간 사용자 플로우 -### 1. Mobile First -- 모바일 화면을 우선 설계하고 태블릿/데스크톱으로 확장 -- 모바일에서 핵심 기능이 모두 동작해야 함 -- 터치 인터랙션 최적화 - -### 2. 사용자 중심 설계 -- 업무 지식이 없어도 직관적으로 사용 가능 -- 최소한의 클릭으로 주요 작업 완료 -- 명확한 피드백 제공 - -### 3. 일관성 -- 동일한 기능은 동일한 방식으로 동작 -- 일관된 색상, 타이포그래피, 간격 시스템 -- 플랫폼별 네이티브 패턴 준수 - -### 4. 접근성 -- WCAG 2.1 Level AA 준수 -- 키보드 네비게이션 지원 -- 스크린 리더 호환성 - -### 5. 성능 -- 초기 로딩 시간 3초 이내 -- 인터랙션 반응 시간 100ms 이내 -- 점진적 로딩을 통한 사용자 경험 개선 - ---- - -## 회의록별 대시보드 - -### 화면 개요 - -#### 목적 -- 회의록이 확정된 후 회의 결과를 한눈에 파악할 수 있는 통합 뷰 제공 -- 핵심 정보를 빠르게 확인하고 관련 자료로 이동할 수 있는 허브 역할 - -#### 진입 경로 -1. 회의록 공유 화면에서 "대시보드" 탭 클릭 -2. 전체 대시보드에서 특정 회의록 클릭 후 "대시보드" 탭 선택 - -#### 사용자 스토리 -- 유저스토리 ID: **UFR-MEET-070** -- "회의록 작성자로서 | 나는, 회의 결과를 한눈에 파악하기 위해 | 회의록별 대시보드를 통해 핵심 정보를 조회하고 싶다." - ---- - -### 화면 구조 - -#### 1. Header 영역 - -**구성 요소** -- 뒤로가기 버튼: 회의록 본문으로 복귀 -- 회의 제목: 큰 폰트로 강조 (H2) -- 회의 메타 정보 - - 날짜/시간 (아이콘: 📅) - - 장소 (아이콘: 📍) - - 참석자 수 (아이콘: 👥) -- 탭 네비게이션 - - 회의록 (기본 회의록 본문) - - 대시보드 (현재 화면) - - 타임라인 (시간순 발언 기록) - -**레이아웃** -``` -┌─────────────────────────────────────┐ -│ ← 회의록으로 돌아가기 │ -│ │ -│ 2024 Q4 마케팅 전략 회의 │ -│ 📅 2024-01-15 14:00 │ -│ 📍 본사 대회의실 👥 참석자 5명 │ -│ │ -│ [회의록] [대시보드] [타임라인] │ -└─────────────────────────────────────┘ -``` - -**스타일 가이드** -- 배경색: #FFFFFF -- 하단 경계선: 1px solid #E5E7EB -- 제목 폰트: 24px, Bold, #111827 -- 메타 정보: 14px, Regular, #6B7280 -- 탭 활성화: Primary Color (#00D9B1), 하단 2px 보더 - ---- - -#### 2. 핵심내용 섹션 - -**구성 요소** -1. **섹션 헤더** - - 아이콘: 💡 - - 제목: "핵심내용" - - 하단 경계선: 2px solid #F3F4F6 - -2. **핵심 포인트 리스트** (3-5개) - - 번호 배지 (1, 2, 3...) - - 핵심 내용 텍스트 - - 좌측 Primary Color 보더 (3px) - -3. **주요 키워드 태그** - - 해시태그 형식 (#디지털마케팅, #예산증액 등) - - 클릭 시 관련 섹션으로 이동 또는 관련 문서 검색 - -4. **회의 통계** - - 참석자 수 - - 회의 시간 - - 발언 횟수 - - 주요 의제 수 - -**레이아웃** -``` -┌─────────────────────────────────────┐ -│ 💡 핵심내용 │ -├─────────────────────────────────────┤ -│ ┃ ① Q4 마케팅 예산을 전년 대비 │ -│ ┃ 30% 증액하여 디지털 채널... │ -│ ┃ │ -│ ┃ ② 신규 인플루언서 마케팅 캠페인 │ -│ ┃ 을 2월부터 시작하며... │ -│ ┃ │ -│ ┃ ③ 경쟁사 분석 결과를 바탕으로... │ -│ │ -│ #디지털마케팅 #예산증액 #인플루언서│ -│ │ -│ ┌──────┬──────┬──────┬──────┐ │ -│ │ 5명 │ 90분 │ 32회 │ 8개 │ │ -│ │참석자│회의 │발언 │주요 │ │ -│ │ │시간 │횟수 │의제 │ │ -│ └──────┴──────┴──────┴──────┘ │ -└─────────────────────────────────────┘ -``` - -**인터랙션** -- 핵심 포인트 클릭: 해당 내용이 언급된 회의록 위치로 이동 -- 키워드 태그 클릭: 관련 회의록/문서 검색 -- 통계 항목 클릭: 상세 분석 정보 표시 (모달) - -**데이터 요구사항** -- AI 분석 결과 (UFR-AI-010) - - 핵심 포인트 텍스트 배열 (3-5개) - - 주요 키워드 배열 - - 회의록 섹션 링크 정보 -- 회의 통계 데이터 - - 참석자 수 - - 회의 시작/종료 시간 - - 발언 횟수 (화자별) - - 의제 수 - -**에러 처리** -- AI 분석 실패 시: "핵심 내용을 분석하는 중입니다. 잠시만 기다려주세요." 메시지 -- 데이터 로딩 실패 시: "데이터를 불러올 수 없습니다. 새로고침 해주세요." 메시지 - ---- - -#### 3. 결정사항 섹션 - -**구성 요소** -1. **섹션 헤더** - - 아이콘: ✅ - - 제목: "결정사항" - -2. **결정사항 카드** (여러 개) - - 결정 내용 (Bold, 강조) - - 결정자 정보 (아이콘: 👤, 이름, 직책) - - 결정 시간 (아이콘: 🕐, HH:MM) - - 결정 근거/배경 (접을 수 있는 영역) - -**레이아웃** -``` -┌─────────────────────────────────────┐ -│ ✅ 결정사항 │ -├─────────────────────────────────────┤ -│ ┌─────────────────────────────────┐ │ -│ │ Q4 마케팅 예산 30% 증액 승인 │ │ -│ │ (총 3억 → 3.9억) │ │ -│ │ │ │ -│ │ 👤 김민준 (마케팅 본부장) │ │ -│ │ 🕐 14:25 │ │ -│ │ │ │ -│ │ ▼ 배경: 디지털 채널 성과가 │ │ -│ │ 예상을 상회하며, 경쟁사... │ │ -│ └─────────────────────────────────┘ │ -│ │ -│ ┌─────────────────────────────────┐ │ -│ │ 인플루언서 마케팅 캠페인 2월 │ │ -│ │ 1일 론칭 결정 │ │ -│ │ ... │ │ -│ └─────────────────────────────────┘ │ -└─────────────────────────────────────┘ -``` - -**인터랙션** -- 결정사항 카드 클릭: 해당 결정이 논의된 회의록 위치로 이동 -- 배경 영역 클릭: 상세 내용 펼치기/접기 -- 결정자 이름 클릭: 해당 참석자의 발언 목록 보기 - -**데이터 요구사항** -- 결정사항 정보 - - 결정 내용 텍스트 - - 결정자 ID, 이름, 직책 - - 결정 시간 (타임스탬프) - - 결정 근거/배경 텍스트 - - 회의록 섹션 링크 - -**에러 처리** -- 결정사항 없음: "이 회의에서 결정된 사항이 없습니다." -- 데이터 로딩 실패: 재시도 버튼 제공 - ---- - -#### 4. Todo 진행상황 섹션 - -**구성 요소** -1. **섹션 헤더** - - 아이콘: 📋 - - 제목: "Todo 진행상황" - -2. **필터 버튼** - - 전체 (개수) - - 시작 전 (개수) - - 진행 중 (개수) - - 완료 (개수) - -3. **담당자별 Todo 그룹** - - 그룹 헤더 - - 담당자 이름 (아이콘: 👤) - - Todo 개수 배지 - - Todo 카드 (여러 개) - - Todo 제목 - - 진행률 프로그레스 바 (0-100%) - - 마감일 (D-day 형식) - - 우선순위 배지 - -**레이아웃** -``` -┌─────────────────────────────────────┐ -│ 📋 Todo 진행상황 │ -├─────────────────────────────────────┤ -│ [전체 12] [시작 전 3] [진행 중 6] │ -│ [완료 3] │ -│ │ -│ ┌─ 👤 박서연 (4개) ───────────────┐ │ -│ │ ┌─────────────────────────────┐ │ │ -│ │ │ 인플루언서 후보 리스트 작성 │ │ │ -│ │ │ ████████░░ 75% D-5 [높음]│ │ │ -│ │ └─────────────────────────────┘ │ │ -│ │ ┌─────────────────────────────┐ │ │ -│ │ │ 디지털 채널별 예산 배분... │ │ │ -│ │ │ ████░░░░░░ 40% D-2 [긴급]│ │ │ -│ │ └─────────────────────────────┘ │ │ -│ └─────────────────────────────────┘ │ -│ │ -│ ┌─ 👤 이준호 (3개) ───────────────┐ │ -│ │ ... │ │ -│ └─────────────────────────────────┘ │ -└─────────────────────────────────────┘ -``` - -**인터랙션** -- 필터 버튼 클릭: 해당 상태의 Todo만 표시 -- Todo 카드 클릭: Todo 상세 정보 모달 표시 또는 Todo 관리 화면으로 이동 -- 진행률 바 클릭: 진행률 업데이트 UI 표시 -- 담당자 그룹 헤더 클릭: 그룹 접기/펼치기 - -**데이터 요구사항** -- Todo 목록 (UFR-TODO-010 연동) - - Todo ID - - 제목 - - 담당자 ID, 이름 - - 진행률 (0-100%) - - 상태 (시작 전/진행 중/완료) - - 마감일 - - 우선순위 (낮음/보통/높음/긴급) - - 회의록 섹션 링크 - -**실시간 업데이트** -- WebSocket 연결을 통해 Todo 진행상황 실시간 반영 (UFR-TODO-020) -- 진행률 변경 시 프로그레스 바 애니메이션 -- 상태 변경 시 해당 Todo 이동 애니메이션 - -**에러 처리** -- Todo 없음: "할당된 Todo가 없습니다." -- 데이터 로딩 실패: 재시도 버튼 제공 -- 실시간 연결 끊김: "실시간 업데이트가 일시 중단되었습니다." 경고 표시 - ---- - -#### 5. 참고자료 섹션 - -**구성 요소** -1. **섹션 헤더** - - 아이콘: 📚 - - 제목: "참고자료" - -2. **탭 네비게이션** - - 관련 회의록 (개수) - - 프로젝트 문서 (개수) - - 이슈 트래커 (개수) - - 위키 페이지 (개수) - -3. **참고자료 카드** (여러 개) - - 문서 아이콘 (타입별 다른 아이콘) - - 문서 제목 - - 메타 정보 - - 작성일 - - 작성자 - - 관련도 점수 (%) - - 요약 내용 (2-3줄) - -**레이아웃** -``` -┌─────────────────────────────────────┐ -│ 📚 참고자료 │ -├─────────────────────────────────────┤ -│ [관련 회의록 3] [프로젝트 문서 5] │ -│ [이슈 트래커 2] [위키 페이지 4] │ -│ │ -│ ┌─────────────────────────────────┐ │ -│ │ 📄 2024 Q3 마케팅 전략 회의 │ │ -│ │ 2023-12-20 • 김민준 • 92% │ │ -│ │ 이전 분기 마케팅 전략 회의로, │ │ -│ │ 디지털 채널 투자 확대 방향성이 │ │ -│ │ 처음 논의되었으며... │ │ -│ └─────────────────────────────────┘ │ -│ │ -│ ┌─────────────────────────────────┐ │ -│ │ 📄 인플루언서 마케팅 효과 분석 │ │ -│ │ 2024-01-05 • 박서연 • 88% │ │ -│ │ 인플루언서 마케팅 ROI 분석... │ │ -│ └─────────────────────────────────┘ │ -└─────────────────────────────────────┘ -``` - -**인터랙션** -- 탭 클릭: 해당 타입의 참고자료만 표시 -- 참고자료 카드 클릭: 해당 문서로 이동 (새 탭 또는 현재 탭) -- 관련도 점수 클릭: 관련도 계산 근거 표시 (툴팁) - -**데이터 요구사항** -- 관련 회의록 (UFR-AI-040 연동) - - 회의록 ID - - 제목 - - 날짜 - - 참석자 (주 작성자) - - 관련도 점수 (0-100%) - - 요약 내용 (100자 이내) - -- 업무 이력 (UFR-RAG-030 연동) - - 문서 ID - - 문서 타입 (프로젝트 문서/이슈/위키) - - 제목 - - 작성일 - - 작성자 - - 관련도 점수 - - 요약 내용 - -**에러 처리** -- 참고자료 없음: "관련 참고자료가 없습니다." -- 데이터 로딩 실패: 재시도 버튼 제공 -- 문서 링크 오류: "문서를 불러올 수 없습니다." 메시지 - ---- - -### 스타일 시스템 - -#### 색상 팔레트 - -**Primary Color** -- Main: #00D9B1 (Turquoise) -- Light: rgba(0, 217, 177, 0.1) -- Dark: #00B794 - -**Secondary Color** -- Gray Scale - - 900: #111827 - - 800: #1F2937 - - 700: #374151 - - 600: #6B7280 - - 500: #9CA3AF - - 400: #D1D5DB - - 300: #E5E7EB - - 200: #F3F4F6 - - 100: #F9FAFB - - 50: #FAFAFA - -**Status Colors** -- Success: #10B981 -- Warning: #F59E0B -- Error: #EF4444 -- Info: #3B82F6 - -#### 타이포그래피 - -**폰트 패밀리** -- 기본: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif -- 한글: "Noto Sans KR", "Apple SD Gothic Neo", sans-serif - -**폰트 크기** -- H1: 32px / 2rem (Bold) -- H2: 24px / 1.5rem (Bold) -- H3: 20px / 1.25rem (Semibold) -- H4: 18px / 1.125rem (Semibold) -- Body: 16px / 1rem (Regular) -- Body Small: 14px / 0.875rem (Regular) -- Caption: 12px / 0.75rem (Regular) - -**폰트 굵기** -- Bold: 700 -- Semibold: 600 -- Medium: 500 -- Regular: 400 - -**줄 높이** -- 제목: 1.2 -- 본문: 1.6 - -#### 간격 시스템 +### 2.1 주요 사용자 여정 ``` -spacing-1: 4px -spacing-2: 8px -spacing-3: 12px -spacing-4: 16px -spacing-5: 20px -spacing-6: 24px -spacing-8: 32px -spacing-10: 40px -spacing-12: 48px +[인증 플로우] +01-로그인 → 02-대시보드 + +[회의 예약 플로우] +02-대시보드 → 03-회의예약 → 02-대시보드 + +[회의 진행 플로우] +02-대시보드 → 04-템플릿선택 → 05-회의진행 → 06-검증완료 → 07-회의종료 → 08-회의록공유 + +[회의록 관리 플로우] +02-대시보드 → 회의록 상세 조회 → 수정/공유/다운로드 + +[Todo 관리 플로우] +09-Todo관리 → Todo 완료/수정 → 회의록 자동 반영 ``` -#### 경계선 반경 +### 2.2 플로우 다이어그램 ``` -radius-sm: 4px -radius-md: 8px -radius-lg: 12px -radius-xl: 16px -radius-full: 9999px -``` - -#### 그림자 - -``` -shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05) -shadow-md: 0 4px 6px rgba(0, 0, 0, 0.1) -shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.15) +┌─────────┐ +│01-로그인│ +└────┬────┘ + │ + ▼ +┌──────────┐ ┌──────────┐ +│02-대시보드│────▶│03-회의예약│ +└────┬─────┘ └──────────┘ + │ + ├─────────▶┌──────────────┐ + │ │04-템플릿선택 │ + │ └──────┬───────┘ + │ │ + │ ▼ + │ ┌──────────┐ + │ │05-회의진행│ + │ └────┬─────┘ + │ │ + │ ▼ + │ ┌──────────┐ + │ │06-검증완료│ + │ └────┬─────┘ + │ │ + │ ▼ + │ ┌──────────┐ + │ │07-회의종료│ + │ └────┬─────┘ + │ │ + │ ▼ + │ ┌────────────┐ + │ │08-회의록공유│ + │ └────────────┘ + │ + └─────────▶┌──────────┐ + │09-Todo관리│ + └──────────┘ ``` --- -## 반응형 디자인 전략 +## 3. 화면별 상세 설계 -### 브레이크포인트 +### 3.1 01-로그인 +#### 개요 +- **목적**: 사용자 인증 및 서비스 진입 +- **관련 유저스토리**: UFR-USER-010 +- **비즈니스 중요도**: 필수 +- **화면 타입**: 단일 목적 페이지 + +#### 주요 기능 +1. 사번과 비밀번호를 통한 LDAP 인증 +2. 세션 관리 및 보안 유지 +3. 인증 실패 시 오류 메시지 표시 + +#### UI 구성요소 + +**Mobile (320px ~ 767px)** ``` -Mobile: 0-767px (기본) -Tablet: 768-1023px -Desktop: 1024px+ +┌─────────────────────────┐ +│ [로고 이미지] │ +│ │ +│ 회의록 작성 서비스 │ +│ │ +│ ┌───────────────────┐ │ +│ │ 사번 │ │ +│ │ [입력 필드] │ │ +│ └───────────────────┘ │ +│ │ +│ ┌───────────────────┐ │ +│ │ 비밀번호 │ │ +│ │ [입력 필드] │ │ +│ └───────────────────┘ │ +│ │ +│ ┌───────────────────┐ │ +│ │ 로그인 버튼 │ │ +│ └───────────────────┘ │ +│ │ +│ LDAP 연동 인증 시스템 │ +└─────────────────────────┘ ``` -### 모바일 (0-767px) +**주요 컴포넌트**: +- 로고 이미지 (SVG, 반응형) +- 사번 입력 필드 (type="text", autocomplete="username") +- 비밀번호 입력 필드 (type="password", autocomplete="current-password") +- 로그인 버튼 (primary action, 44x44px 이상) +- 인증 안내 텍스트 -**레이아웃** +#### 인터랙션 +1. **입력 검증**: + - 실시간 입력 검증 (사번 형식, 비밀번호 입력 여부) + - 포커스 이동: Tab 키로 필드 간 이동 + - Enter 키로 로그인 실행 + +2. **로그인 처리**: + - 버튼 클릭 → 로딩 인디케이터 표시 + - LDAP 인증 진행 + - 성공 시: 대시보드로 자동 이동 + - 실패 시: 오류 메시지 표시 (3초 후 자동 사라짐) + +3. **오류 처리**: + - 인증 실패: "사번 또는 비밀번호가 올바르지 않습니다" + - 네트워크 오류: "연결에 실패했습니다. 다시 시도해주세요" + - 서버 오류: "일시적인 오류가 발생했습니다" + +#### 데이터 요구사항 +- **입력**: 사번 (문자열), 비밀번호 (문자열) +- **출력**: 인증 토큰, 사용자 정보 (이름, 권한) +- **저장**: 세션 토큰 (로컬 스토리지/세션 스토리지) + +#### 에러 처리 +| 에러 유형 | 메시지 | 액션 | +|-----------|--------|------| +| 인증 실패 | 사번 또는 비밀번호가 올바르지 않습니다 | 재입력 유도 | +| 네트워크 오류 | 연결에 실패했습니다 | 재시도 버튼 표시 | +| 서버 오류 | 일시적인 오류가 발생했습니다 | 관리자 문의 안내 | + +--- + +### 3.2 02-대시보드 + +#### 개요 +- **목적**: 회의록 목록 조회, 빠른 액세스, 상태 확인 +- **관련 유저스토리**: UFR-MEET-045 (상세조회), UFR-MEET-055 (수정) +- **비즈니스 중요도**: 높음 +- **화면 타입**: 목록 및 액션 페이지 + +#### 주요 기능 +1. 회의록 목록 조회 (필터링, 정렬, 검색) +2. 빠른 액션 (새 회의 예약, 진행 중인 회의) +3. 회의록 상세 조회 및 수정 +4. 알림 확인 + +#### UI 구성요소 + +**Mobile (320px ~ 767px)** +``` +┌─────────────────────────────┐ +│ [프로필] 대시보드 [알림🔔] │ +├─────────────────────────────┤ +│ │ +│ ┌─────────────────────┐ │ +│ │ ➕ 새 회의 예약 │ │ +│ └─────────────────────┘ │ +│ │ +│ 🔴 진행 중인 회의 (1건) │ +│ [회의 제목 - 지금 참여] │ +│ │ +├─────────────────────────────┤ +│ 내 회의록 │ +│ │ +│ [전체 ▼] [최신순 ▼] [🔍] │ +│ │ +│ ┌───────────────────────┐ │ +│ │ 📝 프로젝트 킥오프 │ │ +│ │ 2025-10-20 14:00 │ │ +│ │ ✅ 확정완료 │ │ +│ │ 3명 참석 │ │ +│ └───────────────────────┘ │ +│ │ +│ ┌───────────────────────┐ │ +│ │ 📝 주간 회의 │ │ +│ │ 2025-10-19 10:00 │ │ +│ │ ⚠️ 작성중 (60%) │ │ +│ │ 5명 참석 │ │ +│ └───────────────────────┘ │ +│ │ +│ [더보기...] │ +│ │ +├─────────────────────────────┤ +│ [대시보드] [Todo] [더보기]│ +└─────────────────────────────┘ +``` + +**주요 컴포넌트**: +- 상단 헤더: 프로필 아이콘, 타이틀, 알림 아이콘 +- 빠른 액션 버튼: 새 회의 예약 (primary), 진행 중인 회의 (secondary) +- 필터 영역: 상태 필터 (전체/작성중/확정완료), 정렬 (최신순/회의일시순/제목순) +- 검색 바: 회의 제목, 참석자, 키워드 검색 +- 회의록 카드: 제목, 날짜, 상태, 참석자 수, 진행률 (작성중인 경우) +- 하단 네비게이션: 대시보드, Todo, 더보기 + +#### 인터랙션 +1. **회의록 목록**: + - 무한 스크롤 또는 페이지네이션 + - 카드 탭: 상세 화면으로 이동 + - 스와이프: 빠른 액션 (공유, 삭제) + +2. **필터링 및 검색**: + - 필터 선택: 즉시 목록 갱신 + - 검색: 300ms 디바운싱 후 API 요청 + - 결과 없을 시: "검색 결과가 없습니다" 메시지 + +3. **빠른 액션**: + - 새 회의 예약: 03-회의예약 화면으로 이동 + - 진행 중인 회의: 05-회의진행 화면으로 복귀 + +4. **회의록 상세 조회** (UFR-MEET-045): + - 카드 클릭 → 상세 화면 모달 또는 새 페이지 + - 표시 정보: + - 회의 기본 정보: 제목, 일시, 장소, 참석자, 템플릿, 상태, 작성자 + - 섹션별 내용: 논의사항, 결정사항, Todo, 기타 + - 검증 상태: 섹션별 체크 표시 + - Todo 항목: 담당자, 마감일, 상태, 우선순위 + - 첨부파일: 다운로드 링크 + - 부가 기능: + - 수정 버튼 (권한 있는 경우) + - 공유 버튼 + - PDF 다운로드 + - 이전/다음 회의록 네비게이션 + - 뒤로가기 + +5. **회의록 수정** (UFR-MEET-055): + - 수정 버튼 클릭 → 편집 모드 전환 + - 상태별 수정 범위: + - 작성중: 모든 섹션 수정 가능 + - 확정완료: 승인 요청 후 수정 가능 + - 자동 저장: 30초 간격 + - 수정 이력: 누가, 언제, 무엇을 수정했는지 + - 저장 버튼 클릭 → 상태 업데이트 → 목록 갱신 + +#### 데이터 요구사항 +- **입력**: 필터 조건, 검색 키워드, 정렬 옵션 +- **출력**: 회의록 목록 (제목, 날짜, 상태, 참석자, 진행률) +- **캐싱**: 최근 조회한 목록 (5분) + +#### 에러 처리 +| 에러 유형 | 메시지 | 액션 | +|-----------|--------|------| +| 조회 실패 | 목록을 불러올 수 없습니다 | 새로고침 버튼 | +| 검색 실패 | 검색 중 오류가 발생했습니다 | 재시도 유도 | +| 수정 권한 없음 | 수정 권한이 없습니다 | 확인 버튼 | + +--- + +### 3.3 03-회의예약 + +#### 개요 +- **목적**: 회의 예약 및 참석자 초대 +- **관련 유저스토리**: UFR-MEET-010 +- **비즈니스 중요도**: 높음 +- **화면 타입**: 폼 입력 페이지 + +#### 주요 기능 +1. 회의 정보 입력 (제목, 날짜/시간, 장소, 참석자) +2. 입력 검증 및 예약 생성 +3. 초대 이메일 자동 발송 +4. 캘린더 자동 등록 + +#### UI 구성요소 + +**Mobile (320px ~ 767px)** +``` +┌─────────────────────────────┐ +│ [←] 회의 예약 [저장]│ +├─────────────────────────────┤ +│ │ +│ 회의 제목 * │ +│ ┌─────────────────────┐ │ +│ │ [입력 필드] │ │ +│ └─────────────────────┘ │ +│ │ +│ 날짜 및 시간 * │ +│ ┌──────────┬──────────┐ │ +│ │ 2025-10-20│ 14:00 │ │ +│ └──────────┴──────────┘ │ +│ │ +│ 장소 (선택) │ +│ ┌─────────────────────┐ │ +│ │ [입력 필드] │ │ +│ └─────────────────────┘ │ +│ │ +│ 참석자 * │ +│ ┌─────────────────────┐ │ +│ │ user1@company.com │ │ +│ │ user2@company.com │ │ +│ │ + 참석자 추가 │ │ +│ └─────────────────────┘ │ +│ │ +│ ☑️ 회의 시작 30분 전 │ +│ 리마인더 발송 │ +│ │ +│ ┌─────────────────────┐ │ +│ │ 회의 예약하기 │ │ +│ └─────────────────────┘ │ +│ │ +└─────────────────────────────┘ +``` + +**주요 컴포넌트**: +- 상단 헤더: 뒤로가기, 타이틀, 저장 버튼 +- 회의 제목 입력 필드 (최대 100자, 필수) +- 날짜 선택기 (달력 UI, 필수) +- 시간 선택기 (시간 목록, 필수) +- 장소 입력 필드 (최대 200자, 선택) +- 참석자 입력 영역 (이메일 칩, 최소 1명 필수) +- 리마인더 설정 체크박스 +- 예약하기 버튼 (primary action) + +#### 인터랙션 +1. **폼 입력**: + - 실시간 검증: 제목 길이, 이메일 형식, 날짜/시간 유효성 + - 참석자 추가: 이메일 입력 후 Enter 또는 버튼 클릭 + - 참석자 제거: 칩 클릭 또는 스와이프 + +2. **날짜/시간 선택**: + - 날짜 선택기: 달력 모달 표시 + - 시간 선택기: 드롭다운 또는 스크롤 선택 + - 과거 날짜 선택 불가 + +3. **예약 처리**: + - 버튼 클릭 → 필수 항목 검증 + - 성공 시: + - 회의 ID 생성 + - 캘린더 자동 등록 + - 참석자에게 이메일 발송 + - 성공 메시지 표시 (토스트) + - 대시보드로 이동 + - 실패 시: 오류 메시지 표시 + +4. **자동 저장**: + - 입력 중 임시 저장 (30초 간격) + - 뒤로가기 시 저장 확인 다이얼로그 + +#### 데이터 요구사항 +- **입력**: 제목, 날짜, 시간, 장소, 참석자 목록 +- **출력**: 회의 ID, 예약 확인 +- **저장**: 임시 저장 데이터 (로컬 스토리지) + +#### 에러 처리 +| 에러 유형 | 메시지 | 액션 | +|-----------|--------|------| +| 필수 항목 누락 | [항목명]을 입력해주세요 | 해당 필드로 포커스 | +| 이메일 형식 오류 | 올바른 이메일을 입력해주세요 | 재입력 유도 | +| 예약 실패 | 예약에 실패했습니다 | 재시도 버튼 | +| 날짜 과거 선택 | 과거 날짜는 선택할 수 없습니다 | 오늘 날짜로 초기화 | + +--- + +### 3.4 04-템플릿선택 + +#### 개요 +- **목적**: 회의 유형에 맞는 템플릿 선택 및 커스터마이징 +- **관련 유저스토리**: UFR-MEET-020 +- **비즈니스 중요도**: 중간 +- **화면 타입**: 선택 및 설정 페이지 + +#### 주요 기능 +1. 템플릿 목록 표시 (일반, 스크럼, 킥오프, 주간) +2. 템플릿 미리보기 +3. 섹션 커스터마이징 (추가/삭제/순서 변경) +4. 회의록 도구 준비 + +#### UI 구성요소 + +**Mobile (320px ~ 767px)** +``` +┌─────────────────────────────┐ +│ [←] 템플릿 선택 [다음]│ +├─────────────────────────────┤ +│ │ +│ 회의 유형에 맞는 템플릿을 │ +│ 선택해주세요 │ +│ │ +│ ┌───────────────────────┐ │ +│ │ 📋 일반 회의 │ │ +│ │ 기본 구조: 참석자, │ │ +│ │ 안건, 논의, 결정, Todo│ │ +│ │ [미리보기] [✓ 선택] │ │ +│ └───────────────────────┘ │ +│ │ +│ ┌───────────────────────┐ │ +│ │ 🏃 스크럼 회의 │ │ +│ │ 어제 한 일, 오늘 할 일│ │ +│ │ 이슈 │ │ +│ │ [미리보기] [ 선택] │ │ +│ └───────────────────────┘ │ +│ │ +│ ┌───────────────────────┐ │ +│ │ 🚀 프로젝트 킥오프 │ │ +│ │ 개요, 목표, 일정, │ │ +│ │ 역할, 리스크 │ │ +│ │ [미리보기] [ 선택] │ │ +│ └───────────────────────┘ │ +│ │ +│ ┌───────────────────────┐ │ +│ │ 📊 주간 회의 │ │ +│ │ 실적, 이슈, 다음 계획 │ │ +│ │ [미리보기] [ 선택] │ │ +│ └───────────────────────┘ │ +│ │ +│ [커스터마이징] │ +│ │ +└─────────────────────────────┘ +``` + +**주요 컴포넌트**: +- 상단 헤더: 뒤로가기, 타이틀, 다음 버튼 +- 템플릿 카드: 아이콘, 제목, 설명, 미리보기 버튼, 선택 버튼 +- 커스터마이징 버튼 (선택한 템플릿이 있을 때 활성화) + +**커스터마이징 모달**: +``` +┌─────────────────────────────┐ +│ 템플릿 커스터마이징 [완료]│ +├─────────────────────────────┤ +│ │ +│ 섹션 관리 │ +│ │ +│ ☰ 참석자 │ +│ ☰ 안건 │ +│ ☰ 논의 내용 │ +│ ☰ 결정 사항 │ +│ ☰ Todo │ +│ │ +│ ┌─────────────────────┐ │ +│ │ + 섹션 추가 │ │ +│ └─────────────────────┘ │ +│ │ +│ 섹션을 길게 눌러 순서를 │ +│ 변경하거나 삭제할 수 있습니다│ +│ │ +└─────────────────────────────┘ +``` + +#### 인터랙션 +1. **템플릿 선택**: + - 카드 탭: 선택 상태 토글 + - 미리보기 버튼: 템플릿 구조 모달 표시 + - 하나만 선택 가능 (라디오 버튼 방식) + +2. **미리보기**: + - 템플릿 구조 전체 표시 + - 샘플 데이터로 예시 제공 + - 닫기 버튼으로 모달 종료 + +3. **커스터마이징**: + - 커스터마이징 버튼 클릭 → 모달 표시 + - 섹션 순서 변경: 드래그 앤 드롭 또는 위/아래 버튼 + - 섹션 삭제: 스와이프 또는 삭제 아이콘 + - 섹션 추가: 입력 필드에 섹션명 입력 후 추가 + - 완료 버튼: 변경 사항 저장 + +4. **다음 단계**: + - 다음 버튼 클릭 → 05-회의진행 화면으로 이동 + - 템플릿 정보 전달 (선택한 템플릿, 커스터마이징 내용) + +#### 데이터 요구사항 +- **입력**: 템플릿 선택, 커스터마이징 정보 +- **출력**: 준비된 회의록 구조 +- **캐싱**: 템플릿 목록 (앱 시작 시 로드) + +#### 에러 처리 +| 에러 유형 | 메시지 | 액션 | +|-----------|--------|------| +| 템플릿 미선택 | 템플릿을 선택해주세요 | 선택 유도 | +| 섹션명 중복 | 이미 존재하는 섹션명입니다 | 재입력 유도 | +| 섹션 로드 실패 | 템플릿을 불러올 수 없습니다 | 새로고침 버튼 | + +--- + +### 3.5 05-회의진행 + +#### 개요 +- **목적**: 회의 진행, 실시간 회의록 작성, 차별화 기능 제공 +- **관련 유저스토리**: UFR-MEET-030 (회의시작), UFR-AI-010 (자동작성), UFR-RAG-010/020 (용어설명), UFR-COLLAB-010/020 (협업) +- **비즈니스 중요도**: 매우 높음 (핵심 차별화 기능) +- **화면 타입**: 실시간 협업 페이지 + +#### 주요 기능 +1. 음성 녹음 및 실시간 STT 변환 +2. AI 자동 회의록 작성 및 구조화 +3. 맥락 기반 용어 설명 (차별화) +4. 실시간 협업 및 동기화 +5. 충돌 해결 +6. 회의 종료 + +#### UI 구성요소 + +**Mobile (320px ~ 767px)** +``` +┌─────────────────────────────┐ +│ [←] 프로젝트 킥오프 [종료]│ +├─────────────────────────────┤ +│ │ +│ 🔴 녹음 중 [23:45] │ +│ ┌─────────────────────┐ │ +│ │ 🎵 파형 애니메이션 │ │ +│ └─────────────────────┘ │ +│ │ +│ 👥 참석자 (3/5명) │ +│ [김민준] [박서연] [이준호] │ +│ │ +├─────────────────────────────┤ +│ 📝 실시간 회의록 │ +│ │ +│ ▼ 참석자 │ +│ - 김민준 (주관자) │ +│ - 박서연 │ +│ - 이준호 │ +│ │ +│ ▼ 안건 │ +│ - 프로젝트 목표 정의 │ +│ - 일정 및 마일스톤 │ +│ │ +│ ▼ 논의 내용 │ +│ "우리는 Q1까지 MVP를 │ +│ 완성해야 합니다..." │ +│ │ +│ 💡 [RAG] 용어 설명 │ +│ "MVP는 최소 기능 제품으로│ +│ 이전 프로젝트에서는..." │ +│ │ +│ ▼ 결정 사항 │ +│ - 개발 프레임워크: React │ +│ - 배포 환경: AWS │ +│ │ +│ ▼ Todo │ +│ ☐ 요구사항 정의 @김민준 │ +│ (~ 10/25) │ +│ │ +│ [📝 수정] [💬 댓글] │ +│ │ +└─────────────────────────────┘ +``` + +**주요 컴포넌트**: +- 상단 헤더: 뒤로가기, 회의 제목, 종료 버튼 +- 녹음 상태 영역: 녹음 표시, 진행 시간, 파형 애니메이션 +- 참석자 목록: 아바타, 이름, 참석 상태 +- 회의록 섹션: 아코디언 방식, 접기/펼치기 +- 실시간 텍스트 영역: STT 변환 결과 표시 +- AI 자동 정리 영역: 구조화된 회의록 +- 용어 하이라이트: 밑줄 또는 배경색, 툴팁 +- 액션 버튼: 수정, 댓글, 첨부 + +**맥락 기반 용어 설명 툴팁** (차별화 기능): +``` +┌─────────────────────────────┐ +│ MVP (Minimum Viable Product)│ +├─────────────────────────────┤ +│ 📘 정의: │ +│ 최소 기능 제품, 핵심 기능만 │ +│ 구현하여 시장 검증 │ +│ │ +│ 🏢 이 회의에서의 의미: │ +│ "Q1까지 사용자 인증, 대시보드│ +│ 핵심 기능만 구현" │ +│ │ +│ 📂 관련 프로젝트: │ +│ - 2024 고객 포털 프로젝트 │ +│ - 2023 모바일 앱 리뉴얼 │ +│ │ +│ 📄 과거 회의록: │ +│ - 2024-09-15 기획 회의 │ +│ - 2024-08-20 킥오프 회의 │ +│ │ +│ [자세히 보기] │ +└─────────────────────────────┘ +``` + +#### 인터랙션 +1. **회의 시작**: + - 회의 시작 버튼 클릭 → 권한 확인 (마이크) + - 녹음 시작 → 파형 애니메이션 표시 + - 참석자 목록 표시 → 실시간 참석 상태 업데이트 + +2. **실시간 STT 및 AI 작성**: + - 음성 인식 → 텍스트 변환 (1초 이내) + - AI 자동 정리 → 3-5초 간격으로 회의록 업데이트 + - 화자 식별 → 발언자 표시 + - 구조화 → 템플릿 섹션에 맞춰 자동 분류 + +3. **맥락 기반 용어 설명** (차별화): + - 전문용어 자동 감지 → 하이라이트 표시 + - 용어 클릭/탭 → 툴팁 표시 + - RAG 검색 → 과거 회의록, 사내 문서에서 맥락 추출 + - 설명 표시: + - 간단한 정의 + - 이 회의에서의 의미 + - 관련 프로젝트/이슈 + - 과거 회의록 링크 + - 사내 문서 링크 + - 툴팁 외부 클릭 → 닫기 + - "자세히 보기" → 전체 화면 모달 + +4. **실시간 협업** (UFR-COLLAB-010): + - 회의록 수정 → WebSocket으로 즉시 동기화 + - 수정 영역 하이라이트 (3초간) + - 수정자 이름 표시 + - 버전 관리 → 수정 이력 저장 + +5. **충돌 해결** (UFR-COLLAB-020): + - 동일 위치 동시 수정 감지 + - 충돌 알림 표시 + - 해결 방식 선택: + - Last Write Wins (기본) + - 수동 병합 (비교 UI 표시) + +6. **회의 종료**: + - 종료 버튼 클릭 → 확인 다이얼로그 + - 확인 → 녹음 중지 → 06-검증완료 화면으로 이동 + +#### 데이터 요구사항 +- **입력**: 음성 스트림, 회의 ID, 참석자 정보 +- **출력**: 텍스트 변환 결과, 구조화된 회의록, 용어 설명 +- **실시간 동기화**: WebSocket 연결 +- **로컬 저장**: 임시 회의록 (30초 간격) + +#### 에러 처리 +| 에러 유형 | 메시지 | 액션 | +|-----------|--------|------| +| 마이크 권한 없음 | 마이크 권한이 필요합니다 | 설정 안내 | +| STT 실패 | 음성 인식에 실패했습니다 | 수동 입력 모드 | +| AI 처리 실패 | 자동 정리 중 오류 발생 | 재시도 버튼 | +| 동기화 실패 | 연결이 끊어졌습니다 | 재연결 시도 | +| 용어 설명 없음 | 관련 정보를 찾을 수 없습니다 | 전문가 요청 버튼 | + +--- + +### 3.6 06-검증완료 + +#### 개요 +- **목적**: 회의록 섹션별 검증 및 완료 표시 +- **관련 유저스토리**: UFR-COLLAB-030 +- **비즈니스 중요도**: 중간 +- **화면 타입**: 검증 및 확인 페이지 + +#### 주요 기능 +1. 섹션별 검증 상태 확인 +2. 검증 완료 체크 +3. 섹션 잠금 (회의 생성자만) +4. 다음 단계 (회의 종료) + +#### UI 구성요소 + +**Mobile (320px ~ 767px)** +``` +┌─────────────────────────────┐ +│ [←] 회의록 검증 [다음]│ +├─────────────────────────────┤ +│ │ +│ 회의록 섹션별로 검증해주세요│ +│ │ +│ ✅ 참석자 (검증완료) │ +│ ┌───────────────────────┐ │ +│ │ - 김민준 (주관자) │ │ +│ │ - 박서연 │ │ +│ │ - 이준호 │ │ +│ │ │ │ +│ │ 검증자: 김민준 │ │ +│ │ 시간: 14:35 │ │ +│ │ [수정] [🔒 잠금] │ │ +│ └───────────────────────┘ │ +│ │ +│ ⚠️ 안건 (검증 필요) │ +│ ┌───────────────────────┐ │ +│ │ - 프로젝트 목표 정의 │ │ +│ │ - 일정 및 마일스톤 │ │ +│ │ │ │ +│ │ [수정] [✓ 검증완료] │ │ +│ └───────────────────────┘ │ +│ │ +│ ⚠️ 논의 내용 (검증 필요) │ +│ ┌───────────────────────┐ │ +│ │ "우리는 Q1까지..." │ │ +│ │ │ │ +│ │ [수정] [✓ 검증완료] │ │ +│ └───────────────────────┘ │ +│ │ +│ ✅ 결정 사항 (검증완료) │ +│ ✅ Todo (검증완료) │ +│ │ +│ 전체 진행률: 60% (3/5) │ +│ │ +└─────────────────────────────┘ +``` + +**주요 컴포넌트**: +- 상단 헤더: 뒤로가기, 타이틀, 다음 버튼 +- 섹션 카드: 제목, 내용 미리보기, 검증 상태, 검증자 정보 +- 액션 버튼: 수정, 검증완료, 잠금 (회의 생성자만) +- 진행률 표시: 퍼센트, 완료 수/전체 수 + +#### 인터랙션 +1. **섹션 검증**: + - 섹션 카드 탭 → 전체 내용 표시 + - 검증완료 버튼 클릭 → 상태 업데이트 + - 검증 시간 및 검증자 기록 + - 실시간 동기화 → 다른 참석자에게 알림 + +2. **섹션 잠금** (회의 생성자만): + - 잠금 버튼 클릭 → 확인 다이얼로그 + - 확인 → 섹션 잠금 (추가 수정 불가) + - 잠금 아이콘 표시 + +3. **섹션 수정**: + - 수정 버튼 클릭 → 편집 모드 + - 인라인 편집 또는 모달 + - 저장 → 상태 "검증 필요"로 변경 + +4. **다음 단계**: + - 다음 버튼 클릭 → 07-회의종료 화면으로 이동 + - 검증 미완료 섹션이 있어도 진행 가능 (나중에 수정 가능) + +#### 데이터 요구사항 +- **입력**: 섹션별 검증 상태 +- **출력**: 검증 완료 정보 (검증자, 시간) +- **실시간 동기화**: WebSocket + +#### 에러 처리 +| 에러 유형 | 메시지 | 액션 | +|-----------|--------|------| +| 검증 권한 없음 | 검증 권한이 없습니다 | 확인 버튼 | +| 잠금 권한 없음 | 회의 생성자만 잠금할 수 있습니다 | 확인 버튼 | +| 동기화 실패 | 검증 상태 동기화 실패 | 재시도 버튼 | + +--- + +### 3.7 07-회의종료 + +#### 개요 +- **목적**: 회의 종료, 통계 확인, Todo 자동 추출, 최종 확정 +- **관련 유저스토리**: UFR-MEET-040 (회의종료), UFR-MEET-050 (최종확정), UFR-AI-020 (Todo자동추출) +- **비즈니스 중요도**: 높음 +- **화면 타입**: 요약 및 확정 페이지 + +#### 주요 기능 +1. 회의 통계 표시 +2. AI Todo 자동 추출 결과 +3. 최종 회의록 확정 +4. 회의록 공유 이동 + +#### UI 구성요소 + +**Mobile (320px ~ 767px)** +``` +┌─────────────────────────────┐ +│ [←] 회의 종료 [확정]│ +├─────────────────────────────┤ +│ │ +│ 🎉 회의가 종료되었습니다 │ +│ │ +│ 📊 회의 통계 │ +│ ┌───────────────────────┐ │ +│ │ ⏱️ 총 시간: 45분 │ │ +│ │ 👥 참석자: 3명 │ │ +│ │ 💬 발언 횟수: │ │ +│ │ 김민준 12회 │ │ +│ │ 박서연 8회 │ │ +│ │ 이준호 5회 │ │ +│ │ 🔑 주요 키워드: │ │ +│ │ #MVP #React #AWS │ │ +│ └───────────────────────┘ │ +│ │ +│ ✅ AI Todo 자동 추출 │ +│ ┌───────────────────────┐ │ +│ │ ☐ 요구사항 정의 │ │ +│ │ @김민준 ~ 10/25 │ │ +│ │ │ │ +│ │ ☐ 기술 스택 검토 │ │ +│ │ @박서연 ~ 10/27 │ │ +│ │ │ │ +│ │ ☐ 인프라 설계 │ │ +│ │ @이준호 ~ 10/30 │ │ +│ └───────────────────────┘ │ +│ │ +│ 필수 항목 확인: │ +│ ✅ 회의 제목 │ +│ ✅ 참석자 목록 │ +│ ✅ 주요 논의 내용 │ +│ ✅ 결정 사항 │ +│ │ +│ ┌─────────────────────┐ │ +│ │ 최종 회의록 확정 │ │ +│ └─────────────────────┘ │ +│ │ +│ [나중에 확정] │ +│ │ +└─────────────────────────────┘ +``` + +**주요 컴포넌트**: +- 상단 헤더: 뒤로가기, 타이틀, 확정 버튼 +- 완료 메시지: 아이콘, 텍스트 +- 통계 카드: 시간, 참석자, 발언 횟수, 키워드 +- Todo 자동 추출 영역: Todo 목록, 담당자, 마감일 +- 필수 항목 체크리스트: 체크 아이콘 +- 확정 버튼 (primary) +- 나중에 확정 버튼 (secondary) + +#### 인터랙션 +1. **회의 통계**: + - 자동 생성 및 표시 + - 그래프 또는 차트 (선택) + - 키워드 탭 → 관련 섹션으로 이동 + +2. **AI Todo 자동 추출** (UFR-AI-020): + - AI가 회의록 분석 → 액션 아이템 식별 + - 담당자 자동 지정 (발언 기반) + - 마감일 자동 추출 (언급된 경우) + - Todo 편집 가능 (담당자, 마감일 수정) + - 추가/삭제 가능 + +3. **최종 확정** (UFR-MEET-050): + - 확정 버튼 클릭 → 필수 항목 검사 + - 필수 항목 누락 시: + - 누락 항목 표시 + - 해당 섹션으로 이동 유도 + - 필수 항목 완료 시: + - 최종 버전 생성 + - 확정 시간 기록 + - Todo 서비스로 Todo 전달 (UFR-TODO-010) + - 08-회의록공유 화면으로 이동 + +4. **나중에 확정**: + - 버튼 클릭 → 대시보드로 이동 + - 회의록 상태: "작성중" + - 나중에 수정 및 확정 가능 + +#### 데이터 요구사항 +- **입력**: 회의 데이터 (시간, 참석자, 발언 내용) +- **출력**: 통계, Todo 목록, 확정 정보 +- **저장**: 최종 회의록, Todo 데이터 + +#### 에러 처리 +| 에러 유형 | 메시지 | 액션 | +|-----------|--------|------| +| 필수 항목 누락 | [항목명]이 작성되지 않았습니다 | 해당 섹션으로 이동 | +| Todo 추출 실패 | Todo 자동 추출 실패 | 수동 작성 유도 | +| 확정 실패 | 회의록 확정에 실패했습니다 | 재시도 버튼 | + +--- + +### 3.8 08-회의록공유 + +#### 개요 +- **목적**: 회의록 공유 설정 및 알림 발송 +- **관련 유저스토리**: UFR-MEET-060 +- **비즈니스 중요도**: 높음 +- **화면 타입**: 설정 및 액션 페이지 + +#### 주요 기능 +1. 공유 대상 선택 +2. 권한 설정 +3. 공유 방식 선택 +4. 링크 보안 옵션 +5. 공유 실행 및 알림 발송 + +#### UI 구성요소 + +**Mobile (320px ~ 767px)** +``` +┌─────────────────────────────┐ +│ [←] 회의록 공유 [공유]│ +├─────────────────────────────┤ +│ │ +│ 공유 대상 │ +│ ┌───────────────────────┐ │ +│ │ ● 참석자 전체 (기본) │ │ +│ │ ○ 특정 참석자 선택 │ │ +│ └───────────────────────┘ │ +│ │ +│ 공유 권한 │ +│ ┌───────────────────────┐ │ +│ │ ● 읽기 전용 │ │ +│ │ ○ 댓글 가능 │ │ +│ │ ○ 편집 가능 │ │ +│ └───────────────────────┘ │ +│ │ +│ 공유 방식 │ +│ ┌───────────────────────┐ │ +│ │ ☑️ 이메일 발송 │ │ +│ │ ☑️ 링크 복사 │ │ +│ └───────────────────────┘ │ +│ │ +│ 링크 보안 (선택) │ +│ ┌───────────────────────┐ │ +│ │ ☐ 유효 기간 설정 │ │ +│ │ [30일 ▼] │ │ +│ │ │ │ +│ │ ☐ 비밀번호 설정 │ │ +│ │ [입력 필드] │ │ +│ └───────────────────────┘ │ +│ │ +│ 🔔 다음 회의 일정 │ +│ ┌───────────────────────┐ │ +│ │ ☑️ 캘린더 자동 등록 │ │ +│ │ 날짜: [선택] │ │ +│ └───────────────────────┘ │ +│ │ +│ ┌─────────────────────┐ │ +│ │ 회의록 공유 │ │ +│ └─────────────────────┘ │ +│ │ +└─────────────────────────────┘ +``` + +**주요 컴포넌트**: +- 상단 헤더: 뒤로가기, 타이틀, 공유 버튼 +- 공유 대상 라디오 버튼 +- 권한 설정 라디오 버튼 +- 공유 방식 체크박스 +- 링크 보안 옵션: 유효 기간, 비밀번호 +- 다음 회의 일정: 캘린더 자동 등록 옵션 +- 공유 버튼 (primary) + +#### 인터랙션 +1. **공유 대상 선택**: + - 참석자 전체 (기본 선택) + - 특정 참석자 선택 → 체크박스 목록 표시 + +2. **권한 설정**: + - 읽기 전용 (기본) + - 댓글 가능 + - 편집 가능 + +3. **공유 방식**: + - 이메일 발송: 참석자 이메일로 알림 + - 링크 복사: 클립보드에 복사 → 토스트 메시지 + +4. **링크 보안**: + - 유효 기간: 드롭다운 (7일, 30일, 90일, 무제한) + - 비밀번호: 입력 필드, 자동 생성 버튼 + +5. **다음 회의 일정**: + - 회의록에서 다음 회의 언급 자동 감지 + - 캘린더 자동 등록 체크박스 + - 날짜 선택기 + +6. **공유 실행**: + - 공유 버튼 클릭 → 처리 + - 성공 시: + - 공유 링크 생성 + - 이메일 발송 (선택 시) + - 링크 복사 (선택 시) + - 캘린더 등록 (선택 시) + - 공유 시간 기록 + - 성공 메시지 표시 + - 대시보드로 이동 + - 실패 시: 오류 메시지 + +#### 데이터 요구사항 +- **입력**: 공유 대상, 권한, 방식, 보안 옵션 +- **출력**: 공유 링크, 발송 결과 +- **저장**: 공유 이력 + +#### 에러 처리 +| 에러 유형 | 메시지 | 액션 | +|-----------|--------|------| +| 공유 실패 | 공유에 실패했습니다 | 재시도 버튼 | +| 이메일 발송 실패 | 이메일 발송 실패 | 링크 복사 유도 | +| 링크 생성 실패 | 링크 생성 실패 | 재시도 버튼 | + +--- + +### 3.9 09-Todo관리 + +#### 개요 +- **목적**: Todo 할당, 진행 관리, 회의록 실시간 연동 (차별화) +- **관련 유저스토리**: UFR-TODO-010 (Todo할당), UFR-TODO-030 (Todo완료처리) +- **비즈니스 중요도**: 높음 (차별화 기능) +- **화면 타입**: 목록 및 관리 페이지 + +#### 주요 기능 +1. Todo 목록 조회 (진행중/완료) +2. Todo 완료 처리 +3. 회의록 실시간 연동 (차별화) +4. 필터링 및 정렬 +5. 알림 발송 + +#### UI 구성요소 + +**Mobile (320px ~ 767px)** +``` +┌─────────────────────────────┐ +│ [프로필] Todo [알림🔔]│ +├─────────────────────────────┤ +│ │ +│ [진행중 ▼] [마감일순 ▼] │ +│ │ +│ 📌 진행 중 (3건) │ +│ │ +│ ┌───────────────────────┐ │ +│ │ ☐ 요구사항 정의 │ │ +│ │ @김민준 │ │ +│ │ 📅 ~ 10/25 (D-5) │ │ +│ │ ⭐ 높음 │ │ +│ │ 📝 프로젝트 킥오프 │ │ +│ │ (10/20) │ │ +│ └───────────────────────┘ │ +│ │ +│ ┌───────────────────────┐ │ +│ │ ☐ 기술 스택 검토 │ │ +│ │ @박서연 │ │ +│ │ 📅 ~ 10/27 (D-7) │ │ +│ │ ⭐ 보통 │ │ +│ │ 📝 주간 회의 (10/19) │ │ +│ └───────────────────────┘ │ +│ │ +│ ✅ 완료됨 (2건) │ +│ │ +│ ┌───────────────────────┐ │ +│ │ ☑️ 회의 일정 조율 │ │ +│ │ @이준호 │ │ +│ │ ✓ 10/18 완료 │ │ +│ │ 📝 킥오프 준비 회의 │ │ +│ └───────────────────────┘ │ +│ │ +├─────────────────────────────┤ +│ [대시보드] [Todo] [더보기]│ +└─────────────────────────────┘ +``` + +**주요 컴포넌트**: +- 상단 헤더: 프로필, 타이틀, 알림 +- 필터 영역: 상태 (진행중/완료), 정렬 (마감일/우선순위/최신순) +- Todo 카드: + - 체크박스 (완료 처리) + - Todo 내용 + - 담당자 + - 마감일 (D-day 표시) + - 우선순위 (아이콘) + - 회의록 링크 (제목, 날짜) +- 하단 네비게이션 + +**Todo 상세 모달**: +``` +┌─────────────────────────────┐ +│ Todo 상세 [✕] │ +├─────────────────────────────┤ +│ │ +│ ☐ 요구사항 정의 │ +│ │ +│ 담당자: 김민준 │ +│ 마감일: 2025-10-25 │ +│ 우선순위: 높음 │ +│ │ +│ 📝 관련 회의록: │ +│ 프로젝트 킥오프 (10/20) │ +│ [회의록 보기] │ +│ │ +│ 💬 댓글 (2) │ +│ - 김민준: 진행 중입니다 │ +│ - 박서연: 도움 필요하시면 │ +│ 연락주세요 │ +│ │ +│ ┌─────────────────────┐ │ +│ │ 완료 처리 │ │ +│ └─────────────────────┘ │ +│ │ +└─────────────────────────────┘ +``` + +#### 인터랙션 +1. **Todo 목록**: + - 무한 스크롤 또는 페이지네이션 + - 카드 탭: 상세 모달 표시 + - 스와이프: 빠른 액션 (완료, 수정, 삭제) + +2. **Todo 완료 처리** (UFR-TODO-030): + - 체크박스 클릭 → 확인 다이얼로그 + - 확인 → 완료 상태 변경 + - 완료 시간 기록 + - 회의록 실시간 반영 (차별화): + - 관련 회의록의 Todo 섹션 자동 업데이트 + - 완료 표시 (체크 아이콘) + - 완료 시간 및 완료자 표시 + - 알림 발송: + - 회의 참석자에게 완료 알림 + - 모든 Todo 완료 시 전체 완료 알림 + +3. **회의록 연결** (차별화): + - 회의록 보기 버튼 → 해당 회의록 상세 화면 + - 원문 맥락 추적 가능 + - 양방향 연결 (Todo ↔ 회의록) + +4. **필터링 및 정렬**: + - 상태 필터: 진행중, 완료 + - 정렬: 마감일순, 우선순위, 최신순 + +5. **댓글 및 협업**: + - 댓글 작성 → 실시간 동기화 + - 담당자에게 알림 + +#### 데이터 요구사항 +- **입력**: 필터, 정렬, 완료 처리 +- **출력**: Todo 목록, 회의록 연결 정보 +- **실시간 동기화**: WebSocket (회의록 반영) + +#### 에러 처리 +| 에러 유형 | 메시지 | 액션 | +|-----------|--------|------| +| 조회 실패 | Todo 목록을 불러올 수 없습니다 | 새로고침 버튼 | +| 완료 처리 실패 | 완료 처리에 실패했습니다 | 재시도 버튼 | +| 회의록 연결 실패 | 회의록을 찾을 수 없습니다 | 확인 버튼 | + +--- + +## 4. 화면 간 전환 및 네비게이션 + +### 4.1 네비게이션 전략 + +**Mobile (320px ~ 767px)** +- **하단 네비게이션 바**: 대시보드, Todo, 더보기 +- **상단 헤더**: 뒤로가기, 타이틀, 액션 버튼 +- **햄버거 메뉴**: 설정, 프로필, 로그아웃 +- **플로팅 액션 버튼 (FAB)**: 빠른 액세스 (새 회의 예약) + +**Tablet (768px ~ 1023px)** +- **사이드 네비게이션**: 고정된 좌측 메뉴 +- **상단 네비게이션**: 로고, 검색, 알림, 프로필 +- **플로팅 액션 버튼 (FAB)**: 빠른 액세스 + +**Desktop (1024px 이상)** +- **좌측 사이드바**: 고정된 네비게이션 메뉴 +- **상단 헤더**: 로고, 검색, 알림, 프로필 +- **키보드 단축키**: 빠른 네비게이션 + +### 4.2 화면 전환 패턴 + +| 전환 | 애니메이션 | 방향 | +|------|------------|------| +| 로그인 → 대시보드 | 페이드인 | - | +| 대시보드 → 상세 | 슬라이드 좌 | 우 → 좌 | +| 상세 → 대시보드 | 슬라이드 우 | 좌 → 우 | +| 모달 표시 | 슬라이드 상 | 하 → 상 | +| 모달 닫기 | 슬라이드 하 | 상 → 하 | + +### 4.3 제스처 지원 (Mobile) + +- **스와이프 우**: 뒤로가기 (iOS 스타일) +- **스와이프 좌**: 빠른 액션 (삭제, 공유) +- **길게 누르기**: 컨텍스트 메뉴 +- **핀치 줌**: 이미지 확대/축소 +- **풀 투 리프레시**: 목록 새로고침 + +--- + +## 5. 반응형 설계 전략 + +### 5.1 브레이크포인트 + +```css +/* Mobile First (기본) */ +@media (min-width: 320px) { /* Mobile */ } +@media (min-width: 768px) { /* Tablet */ } +@media (min-width: 1024px) { /* Desktop */ } +@media (min-width: 1440px) { /* Large Desktop */ } +``` + +### 5.2 레이아웃 전략 + +**Mobile (320px ~ 767px)** - 단일 컬럼 레이아웃 -- 패딩: 16px (spacing-4) -- 섹션 간 간격: 24px (spacing-6) +- 풀 스크린 모달 +- 하단 네비게이션 +- 스택 방식 (세로 배치) -**회의록별 대시보드 최적화** -- 통계 카드: 2x2 그리드 -- Todo 그룹: 기본 펼침 상태, 스와이프로 접기/펼치기 -- 참고자료 탭: 가로 스크롤 가능 +**Tablet (768px ~ 1023px)** +- 2단 컬럼 (메인 + 사이드) +- 사이드 패널 (용어 설명, 참석자 목록) +- 플로팅 모달 +- 그리드 레이아웃 (회의록 목록 2열) -**인터랙션** -- 터치 타겟 최소 크기: 44x44px -- 스와이프 제스처 지원 -- 하단 고정 액션 버튼 +**Desktop (1024px 이상)** +- 3단 컬럼 (네비게이션 + 메인 + 사이드) +- 인라인 모달 +- 상단 네비게이션 +- 그리드 레이아웃 (회의록 목록 3-4열) -### 태블릿 (768-1023px) +### 5.3 점진적 향상 -**레이아웃** -- 2컬럼 레이아웃 (일부 섹션) -- 패딩: 24px (spacing-6) -- 섹션 간 간격: 32px (spacing-8) +**Mobile (기본 기능)** +- 핵심 회의록 작성 기능 +- 필수 검증 및 공유 +- 기본 Todo 관리 -**회의록별 대시보드 최적화** -- 통계 카드: 4x1 그리드 -- Todo 그룹: 사이드바에 요약, 메인 영역에 상세 -- 참고자료: 2컬럼 그리드 +**Tablet (추가 기능)** +- 빠른 액세스 패널 +- 회의록 미리보기 +- 멀티 선택 및 일괄 작업 -### 데스크톱 (1024px+) +**Desktop (고급 기능)** +- 다중 회의록 비교 +- 고급 검색 및 필터 +- 키보드 단축키 +- 드래그 앤 드롭 -**레이아웃** -- 3컬럼 레이아웃 (대시보드 등) -- 최대 너비: 1200px (중앙 정렬) -- 패딩: 32px (spacing-8) -- 섹션 간 간격: 48px (spacing-12) +### 5.4 타이포그래피 반응형 -**회의록별 대시보드 최적화** -- 통계 카드: 4x1 그리드 -- Todo 그룹: 좌우 분할 레이아웃 (리스트/상세) -- 참고자료: 3컬럼 그리드 -- 호버 효과 강화 +```css +/* 제목 */ +h1 { + font-size: 24px; /* Mobile */ +} +@media (min-width: 768px) { + h1 { font-size: 32px; } /* Tablet */ +} +@media (min-width: 1024px) { + h1 { font-size: 40px; } /* Desktop */ +} + +/* 본문 */ +body { + font-size: 14px; /* Mobile */ + line-height: 1.5; +} +@media (min-width: 768px) { + body { font-size: 16px; } /* Tablet */ +} +``` + +### 5.5 이미지 및 미디어 반응형 + +- **반응형 이미지**: `srcset`, `sizes` 속성 활용 +- **WebP 포맷**: 최신 브라우저 지원 +- **레이지 로딩**: `loading="lazy"` 속성 +- **비디오**: 자동 재생 비활성화 (모바일) --- -## 접근성 가이드라인 +## 6. 접근성 보장 방안 -### WCAG 2.1 Level AA 준수 +### 6.1 WCAG 2.1 Level AA 준수 -#### 1. 지각 가능성 (Perceivable) +#### 인식 가능성 (Perceivable) -**색상 대비** -- 일반 텍스트: 최소 4.5:1 -- 큰 텍스트 (18px+ Bold, 24px+): 최소 3:1 -- UI 컴포넌트: 최소 3:1 +1. **텍스트 대체**: + - 모든 이미지에 `alt` 텍스트 제공 + - 아이콘에 `aria-label` 추가 + - 정보 전달 이미지: 상세한 설명 + - 장식 이미지: `alt=""` (빈 문자열) -**대체 텍스트** -- 모든 의미 있는 이미지에 alt 속성 제공 -- 아이콘에는 aria-label 또는 visually-hidden 텍스트 제공 +2. **색상 대비**: + - 일반 텍스트: 최소 4.5:1 + - 큰 텍스트 (18pt 이상): 최소 3:1 + - UI 컴포넌트: 최소 3:1 + - 색상만으로 정보 전달 금지 (아이콘, 텍스트 병행) -**색상에만 의존하지 않기** -- 상태 표시 시 색상 + 아이콘/텍스트 병용 -- 예: 에러 상태 = 빨간색 + ⚠️ 아이콘 + "오류" 텍스트 +3. **리사이징**: + - 200%까지 확대 가능 + - 콘텐츠 손실 없음 + - 가로 스크롤 최소화 -#### 2. 작동 가능성 (Operable) +4. **콘텐츠 구조**: + - 의미론적 HTML 사용 (`
`, `
+ + diff --git a/design/uiux_다람지/prototype/02-대시보드.html b/design/uiux_다람지/prototype/02-대시보드.html index 60ca4bb..ee958f4 100644 --- a/design/uiux_다람지/prototype/02-대시보드.html +++ b/design/uiux_다람지/prototype/02-대시보드.html @@ -3,210 +3,223 @@ - 대시보드 - 회의록 작성 서비스 + 대시보드 - 회의록 서비스 + - -
-
KM
-

회의록

-
- -
-
- - -
-
- -
-

오늘의 회의

-
- -
-
- - -
-

최근 회의록

-
- -
-
- - -
-

Todo 요약

-
-
-
-
진행 중
-
-
-
-
-
완료
-
-
-
-
-
전체
-
-
-
-
-
-
+
+ +
+

회의록 서비스

+
+ + +
- - -
+ +
+ +
+

안녕하세요!

+

오늘도 효율적인 회의록 작성을 시작하세요

+
- - + +
+ + +
+ + +
+
+

내 Todo

+ 전체 보기 → +
+ +
+ +
+
+ + +
+
+

내 회의록

+ 전체 보기 → +
+ +
+ +
+
+ + +
+
+

공유받은 회의록

+ 전체 보기 → +
+ +
+

공유받은 회의록이 없습니다

+
+
+
+ + + +
diff --git a/design/uiux_다람지/prototype/03-회의예약.html b/design/uiux_다람지/prototype/03-회의예약.html index e261a67..761059a 100644 --- a/design/uiux_다람지/prototype/03-회의예약.html +++ b/design/uiux_다람지/prototype/03-회의예약.html @@ -3,326 +3,348 @@ - 회의 예약 - 회의록 작성 서비스 + 회의 예약 - 회의록 서비스 + - -
- -

회의 예약

-
- +

회의 예약

+
-
- -
-
+ +
- +
- + - + data-validate="required|maxLength:100" + aria-label="회의 제목" + aria-required="true" + > +

0 / 100

- +
- -
-
- -
-
- -
+ + +
+ + +
+
+ + +
+
+ +
-
- +
-
+ + +
+ + aria-label="회의 장소" + >
- +
- - -
- +
+ +
-
- - + +
+ +
+ +
+ +
+ + +
+ + + +
-
+
diff --git a/design/uiux_다람지/prototype/04-템플릿선택.html b/design/uiux_다람지/prototype/04-템플릿선택.html index dd3ae2e..02be4ee 100644 --- a/design/uiux_다람지/prototype/04-템플릿선택.html +++ b/design/uiux_다람지/prototype/04-템플릿선택.html @@ -3,157 +3,232 @@ - 템플릿 선택 - 회의록 작성 서비스 + 템플릿 선택 - 회의록 서비스 + - -
- -

템플릿 선택

-
- - -
-
-

회의 유형을 선택하세요

- -
- -
- - +

템플릿 선택

+
-
+ + +
+

회의 유형에 맞는 템플릿을 선택하세요. 건너뛰면 일반 템플릿이 사용됩니다.

+ + +
+ +
+
+
diff --git a/design/uiux_다람지/prototype/05-회의진행.html b/design/uiux_다람지/prototype/05-회의진행.html index dcee994..f6c292c 100644 --- a/design/uiux_다람지/prototype/05-회의진행.html +++ b/design/uiux_다람지/prototype/05-회의진행.html @@ -3,236 +3,431 @@ - 회의 진행 - 회의록 작성 서비스 + 회의 진행 - 회의록 서비스 + - -
- -

주간 회의

-
-
-
- -
- 🔴 녹음 중 00:00:00 -
- - -
-
- -
-

참석자 (5명)

-
-
-
-
-
-
+ +
+ +
+
+ mic + 김철수
+

회의를 시작하겠습니다. 오늘은 프로젝트 킥오프 회의로...

- -
-

📝 회의록

-
-

## 참석자

-

- 김민준
- 박서연
- 이준호
- 최유진
- 정도현

- -

## 논의 내용

-

[김민준] 이번 분기 KPI 목표는 매출 20% 증가입니다.

- -

[박서연 typing...]

-
+ +
+ auto_awesome + AI가 발언 내용을 분석하여 회의록을 작성하고 있습니다
- -
- - + +
+
-
+ + +
+ + + +
+ diff --git a/design/uiux_다람지/prototype/06-검증완료.html b/design/uiux_다람지/prototype/06-검증완료.html index 9b8efb0..8167eca 100644 --- a/design/uiux_다람지/prototype/06-검증완료.html +++ b/design/uiux_다람지/prototype/06-검증완료.html @@ -3,266 +3,217 @@ - 회의록 검증 - 회의록 작성 서비스 + 검증 완료 - 회의록 서비스 - + - -
- -

회의록 검증

-
+
+ +
+ +

검증 완료

+
+
- -
-
-
-

주간 회의

-

2025-01-15

+ +
+ +
+

전체 검증 진행률

+
+
+
+
+
+
+ 0% +
+

0 / 0 섹션 검증 완료

- -
-
- 검증 현황 - - 0/5 - -
-
-
-
-
- - + +

섹션별 검증 상태

- + +
+ + +
+ +
-
+
diff --git a/design/uiux_다람지/prototype/07-회의종료.html b/design/uiux_다람지/prototype/07-회의종료.html index b21dc48..f4b97d1 100644 --- a/design/uiux_다람지/prototype/07-회의종료.html +++ b/design/uiux_다람지/prototype/07-회의종료.html @@ -3,241 +3,209 @@ - 회의 종료 - 회의록 작성 서비스 + 회의 종료 - 회의록 서비스 - + - -
- -

회의 종료

-
+
+ +
+

회의가 종료되었습니다

+
+
- -
-
-
-

주간 회의

-

2025-01-15 14:00 - 14:45

+ +
+ +
+
+

회의 제목

+

2025-10-21 10:00 ~ 11:30

-

📊 회의 통계

- - -
- -
45분 30초
-
총 시간
-
- - -
- -
5명
-
참석자
-
- - +
-

💬 발언 횟수

-
- +

회의 통계

+
+ 회의 총 시간 + 01:30:00 +
+
+ 참석자 수 + 3명 +
+
+ 주요 키워드 +
+ Mobile First + AI + 프로젝트 +
- -
-

🔑 주요 키워드

-
- + +
+
+

AI가 추출한 Todo

+ +
+
+
- -
- - + +
-
+
diff --git a/design/uiux_다람지/prototype/08-회의록공유.html b/design/uiux_다람지/prototype/08-회의록공유.html index 92acfd1..8477563 100644 --- a/design/uiux_다람지/prototype/08-회의록공유.html +++ b/design/uiux_다람지/prototype/08-회의록공유.html @@ -3,330 +3,250 @@ - 회의록 공유 - 회의록 작성 서비스 + 회의록 공유 - 회의록 서비스 - + - -
- -

회의록 확정

-
+
+ +
+ +

회의록 공유

+ +
- -
-
- -
-

필수 항목 확인

+ +
+
+ +
+ + + +
-
-
- - 회의 제목 -
-
- - 참석자 목록 -
-
- - 주요 논의 내용 -
-
- - 결정 사항 + + -

선택 항목

-
-
- - Todo 항목 -
+ +
+ +
- -
- - - - - -
+
diff --git a/design/uiux_다람지/prototype/09-Todo관리.html b/design/uiux_다람지/prototype/09-Todo관리.html index 98c558f..611f3fb 100644 --- a/design/uiux_다람지/prototype/09-Todo관리.html +++ b/design/uiux_다람지/prototype/09-Todo관리.html @@ -3,401 +3,278 @@ - Todo 관리 - 회의록 작성 서비스 + Todo 관리 - 회의록 서비스 - + - -
- -

Todo

-
+
+ +
+

내 Todo

+ +
- -
-
- -
- - - + +
+ +
+
+
+
+
+

0

+

전체 Todo

+
+
+

0

+

완료

+
+
+

0

+

마감 임박

+
+
+
+ ${UIComponents.createCircularProgress(0)} +
- -
- + +
+ + + +
- - -
- - + + + + + +
diff --git a/design-last/uiux_다람지/prototype/10-회의록상세조회.html b/design/uiux_다람지/prototype/10-회의록상세조회.html similarity index 100% rename from design-last/uiux_다람지/prototype/10-회의록상세조회.html rename to design/uiux_다람지/prototype/10-회의록상세조회.html diff --git a/design-last/uiux_다람지/prototype/11-회의록수정.html b/design/uiux_다람지/prototype/11-회의록수정.html similarity index 100% rename from design-last/uiux_다람지/prototype/11-회의록수정.html rename to design/uiux_다람지/prototype/11-회의록수정.html diff --git a/design-last/uiux_다람지/prototype/TEST_RESULTS.md b/design/uiux_다람지/prototype/TEST_RESULTS.md similarity index 100% rename from design-last/uiux_다람지/prototype/TEST_RESULTS.md rename to design/uiux_다람지/prototype/TEST_RESULTS.md diff --git a/design/uiux_다람지/prototype/common.css b/design/uiux_다람지/prototype/common.css index bc88d82..bdcf217 100644 --- a/design/uiux_다람지/prototype/common.css +++ b/design/uiux_다람지/prototype/common.css @@ -1,81 +1,113 @@ -/* 회의록 작성 서비스 - 공통 스타일시트 */ +/** + * 회의록 작성 및 공유 개선 서비스 - 공통 스타일시트 + * Mobile First Design + * 작성일: 2025-10-21 + */ -/* ========== CSS Variables ========== */ +/* ======================================== + 1. CSS 변수 정의 + ======================================== */ :root { - /* Colors */ - --primary: #0066CC; - --primary-dark: #004A99; - --primary-light: #E6F2FF; - --text-primary: #1A1A1A; - --text-secondary: #666666; - --text-disabled: #999999; - --text-inverse: #FFFFFF; - --bg-white: #FFFFFF; - --bg-gray: #F5F5F5; - --bg-dark: #1A1A1A; - --success: #0A7029; - --error: #C41E3A; - --warning: #856404; - --info: #0066CC; - --border-light: #E0E0E0; - --border-medium: #CCCCCC; + /* Primary Colors (Blue) */ + --primary-50: #E3F2FD; + --primary-100: #BBDEFB; + --primary-200: #90CAF9; + --primary-300: #64B5F6; + --primary-400: #42A5F5; + --primary-500: #2196F3; + --primary-600: #1E88E5; + --primary-700: #1976D2; + --primary-800: #1565C0; + --primary-900: #0D47A1; - /* Shadows */ - --shadow-sm: 0 1px 3px rgba(0,0,0,0.1); - --shadow-md: 0 4px 6px rgba(0,0,0,0.1); - --shadow-lg: 0 10px 15px rgba(0,0,0,0.1); + /* Secondary Colors */ + --secondary-50: #E8F5E9; + --secondary-100: #C8E6C9; + --secondary-500: #4CAF50; + --secondary-700: #388E3C; + --secondary-900: #1B5E20; - /* Typography */ - --font-primary: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; - --font-mono: "SF Mono", Monaco, "Cascadia Code", monospace; - --font-xs: 0.75rem; - --font-sm: 0.875rem; - --font-base: 1rem; - --font-lg: 1.125rem; - --font-xl: 1.25rem; - --font-2xl: 1.5rem; - --font-3xl: 2rem; - --font-regular: 400; - --font-medium: 500; - --font-semibold: 600; - --font-bold: 700; - --leading-tight: 1.25; - --leading-normal: 1.5; - --leading-relaxed: 1.75; + /* Accent Colors (Purple - AI) */ + --accent-50: #F3E5F5; + --accent-100: #E1BEE7; + --accent-500: #9C27B0; + --accent-700: #7B1FA2; + + /* Semantic Colors */ + --success: #4CAF50; + --success-bg: #E8F5E9; + --warning: #FF9800; + --warning-bg: #FFF3E0; + --error: #F44336; + --error-bg: #FFEBEE; + --info: #2196F3; + --info-bg: #E3F2FD; + + /* Neutral Colors (Gray) */ + --gray-50: #FAFAFA; + --gray-100: #F5F5F5; + --gray-200: #EEEEEE; + --gray-300: #E0E0E0; + --gray-400: #BDBDBD; + --gray-500: #9E9E9E; + --gray-600: #757575; + --gray-700: #616161; + --gray-800: #424242; + --gray-900: #212121; + + /* White & Black */ + --white: #FFFFFF; + --black: #000000; /* Spacing */ - --space-1: 0.25rem; - --space-2: 0.5rem; - --space-3: 0.75rem; - --space-4: 1rem; - --space-5: 1.25rem; - --space-6: 1.5rem; - --space-8: 2rem; - --space-10: 2.5rem; - --space-12: 3rem; - --space-16: 4rem; + --spacing-0: 0; + --spacing-1: 4px; + --spacing-2: 8px; + --spacing-3: 12px; + --spacing-4: 16px; + --spacing-5: 20px; + --spacing-6: 24px; + --spacing-8: 32px; + --spacing-10: 40px; + --spacing-12: 48px; + --spacing-16: 64px; - /* Animation */ - --duration-fast: 150ms; - --duration-base: 200ms; - --duration-slow: 300ms; - --ease-in-out: cubic-bezier(0.4, 0, 0.2, 1); + /* Typography */ + --font-primary: 'Pretendard', -apple-system, BlinkMacSystemFont, system-ui, 'Noto Sans KR', sans-serif; + --font-secondary: 'Inter', 'Roboto', Arial, sans-serif; + --font-code: 'Monaco', 'Courier New', monospace; + + /* Border Radius */ + --radius-sm: 4px; + --radius-md: 8px; + --radius-lg: 12px; + --radius-xl: 16px; + --radius-full: 9999px; + + /* Shadows */ + --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.1); + --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.15); + --shadow-lg: 0 20px 60px rgba(0, 0, 0, 0.3); + + /* Transitions */ + --transition-fast: 0.1s; + --transition-normal: 0.2s; + --transition-slow: 0.3s; + + /* Z-index */ + --z-base: 1; + --z-dropdown: 10; + --z-sticky: 50; + --z-fixed: 100; + --z-modal-backdrop: 1000; + --z-modal: 1001; + --z-toast: 2000; } -/* Dark Mode */ -@media (prefers-color-scheme: dark) { - :root { - --bg-white: #1A1A1A; - --bg-gray: #2A2A2A; - --text-primary: #FFFFFF; - --text-secondary: #CCCCCC; - --border-light: #404040; - --shadow-sm: 0 1px 3px rgba(0,0,0,0.3); - } -} - -/* ========== Reset & Base ========== */ -*, *::before, *::after { +/* ======================================== + 2. Reset & Base Styles + ======================================== */ +* { box-sizing: border-box; margin: 0; padding: 0; @@ -83,201 +115,250 @@ html { font-size: 16px; - -webkit-text-size-adjust: 100%; -} - -body { - font-family: var(--font-primary); - font-size: var(--font-base); - line-height: var(--leading-normal); - color: var(--text-primary); - background-color: var(--bg-gray); -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } -h1, h2, h3, h4, h5, h6 { - font-weight: var(--font-semibold); - line-height: var(--leading-tight); - margin-bottom: var(--space-4); -} - -h1 { font-size: var(--font-3xl); } -h2 { font-size: var(--font-2xl); } -h3 { font-size: var(--font-xl); } -h4 { font-size: var(--font-lg); } - -p { margin-bottom: var(--space-4); } - -a { - color: var(--primary); - text-decoration: none; - transition: color var(--duration-base); -} - -a:hover { color: var(--primary-dark); } - -button { - font-family: inherit; - cursor: pointer; - border: none; - background: none; -} - -input, textarea, select { - font-family: inherit; - font-size: inherit; -} - -/* ========== Layout ========== */ -.container { - width: 100%; - max-width: 100%; - margin: 0 auto; - padding: 0 var(--space-4); -} - -@media (min-width: 768px) { - .container { max-width: 720px; } -} - -@media (min-width: 1024px) { - .container { max-width: 960px; padding: 0 var(--space-8); } -} - -.main-content { +body { + font-family: var(--font-primary); + font-size: 14px; + line-height: 1.5; + color: var(--gray-800); + background-color: var(--gray-50); min-height: 100vh; - padding-bottom: 80px; /* Space for bottom nav on mobile */ } -@media (min-width: 768px) { - .main-content { padding-bottom: 0; } +/* ======================================== + 3. Typography Classes + ======================================== */ +/* Headings */ +.text-h1 { + font-size: 32px; + font-weight: 700; + line-height: 1.2; + color: var(--gray-900); } -/* ========== Header ========== */ -.header { - background: var(--bg-white); - box-shadow: var(--shadow-sm); - padding: var(--space-4); - position: sticky; - top: 0; - z-index: 100; - display: flex; - align-items: center; - justify-content: space-between; +.text-h2 { + font-size: 24px; + font-weight: 700; + line-height: 1.3; + color: var(--gray-900); } -.header-title { - font-size: var(--font-lg); - font-weight: var(--font-semibold); - margin: 0; +.text-h3 { + font-size: 20px; + font-weight: 600; + line-height: 1.4; + color: var(--gray-900); } -.header-back { - background: none; - border: none; - font-size: var(--font-2xl); - color: var(--text-primary); - cursor: pointer; - padding: var(--space-2); +.text-h4 { + font-size: 18px; + font-weight: 600; + line-height: 1.4; + color: var(--gray-900); } -.header-actions { - display: flex; - gap: var(--space-2); +.text-h5 { + font-size: 16px; + font-weight: 500; + line-height: 1.5; + color: var(--gray-900); } -/* ========== Buttons ========== */ +.text-h6 { + font-size: 14px; + font-weight: 500; + line-height: 1.5; + color: var(--gray-900); +} + +/* Body */ +.text-body-lg { + font-size: 16px; + font-weight: 400; + line-height: 1.5; +} + +.text-body { + font-size: 14px; + font-weight: 400; + line-height: 1.5; +} + +.text-body-sm { + font-size: 13px; + font-weight: 400; + line-height: 1.5; +} + +.text-caption { + font-size: 12px; + font-weight: 400; + line-height: 1.4; + color: var(--gray-600); +} + +/* Buttons */ +.text-btn-lg { + font-size: 16px; + font-weight: 600; + line-height: 1.0; +} + +.text-btn { + font-size: 14px; + font-weight: 500; + line-height: 1.0; +} + +.text-btn-sm { + font-size: 13px; + font-weight: 500; + line-height: 1.0; +} + +/* Label */ +.text-label { + font-size: 12px; + font-weight: 500; + line-height: 1.0; + color: var(--gray-700); +} + +/* Code */ +.text-code { + font-family: var(--font-code); + font-size: 14px; + font-weight: 400; + line-height: 1.5; +} + +/* ======================================== + 4. Button Styles + ======================================== */ .btn { display: inline-flex; align-items: center; justify-content: center; - padding: 12px 24px; - border-radius: 8px; - font-weight: var(--font-semibold); - font-size: var(--font-sm); - transition: all var(--duration-base) var(--ease-in-out); - cursor: pointer; + gap: var(--spacing-2); + padding: var(--spacing-3) var(--spacing-6); border: none; + border-radius: var(--radius-md); + font-family: var(--font-primary); + font-size: 16px; + font-weight: 600; + cursor: pointer; + transition: all var(--transition-normal); text-decoration: none; - min-height: 44px; + white-space: nowrap; + user-select: none; } .btn-primary { - background: var(--primary); - color: var(--text-inverse); + background: var(--primary-500); + color: var(--white); + box-shadow: var(--shadow-sm); } -.btn-primary:hover:not(:disabled) { - background: var(--primary-dark); - transform: translateY(-1px); +.btn-primary:hover { + background: var(--primary-600); box-shadow: var(--shadow-md); } -.btn-primary:active:not(:disabled) { - transform: translateY(0); +.btn-primary:active { + background: var(--primary-700); + transform: scale(0.98); +} + +.btn-primary:disabled { + background: var(--gray-200); + color: var(--gray-400); + cursor: not-allowed; + box-shadow: none; } .btn-secondary { + background: var(--white); + color: var(--primary-500); + border: 1px solid var(--primary-500); +} + +.btn-secondary:hover { + background: var(--primary-50); + border-color: var(--primary-600); +} + +.btn-text { background: transparent; - color: var(--primary); - border: 2px solid var(--primary); - padding: 10px 22px; + color: var(--primary-500); + padding: var(--spacing-2) var(--spacing-4); } -.btn-secondary:hover:not(:disabled) { - background: var(--primary-light); -} - -.btn-ghost { - background: transparent; - color: var(--text-primary); -} - -.btn-ghost:hover:not(:disabled) { - background: var(--bg-gray); -} - -.btn-success { - background: var(--success); - color: var(--text-inverse); -} - -.btn-error { - background: var(--error); - color: var(--text-inverse); -} - -.btn:disabled { - opacity: 0.5; - cursor: not-allowed; -} - -.btn-full { - width: 100%; +.btn-text:hover { + background: var(--primary-50); } .btn-icon { - padding: var(--space-3); - border-radius: 50%; - min-height: 44px; - min-width: 44px; + width: 44px; + height: 44px; + padding: 0; + border-radius: var(--radius-full); + background: transparent; } -/* ========== Form Elements ========== */ +.btn-icon:hover { + background: var(--gray-100); +} + +.btn-sm { + padding: var(--spacing-2) var(--spacing-4); + font-size: 14px; +} + +.btn-lg { + padding: var(--spacing-4) var(--spacing-8); + font-size: 18px; +} + +/* FAB (Floating Action Button) */ +.btn-fab { + position: fixed; + bottom: var(--spacing-6); + right: var(--spacing-6); + width: 56px; + height: 56px; + padding: 0; + border-radius: var(--radius-full); + background: var(--primary-500); + color: var(--white); + box-shadow: 0 4px 12px rgba(33, 150, 243, 0.4); + z-index: var(--z-fixed); +} + +.btn-fab:hover { + transform: scale(1.1); + box-shadow: 0 6px 16px rgba(33, 150, 243, 0.5); +} + +/* ======================================== + 5. Form Styles + ======================================== */ .form-group { - margin-bottom: var(--space-6); + margin-bottom: var(--spacing-4); } .form-label { display: block; - font-size: var(--font-sm); - font-weight: var(--font-medium); - color: var(--text-primary); - margin-bottom: var(--space-2); + margin-bottom: var(--spacing-2); + font-size: 12px; + font-weight: 500; + color: var(--gray-700); } -.required { +.form-label.required::after { + content: ' *'; color: var(--error); } @@ -285,382 +366,633 @@ input, textarea, select { .form-textarea, .form-select { width: 100%; - padding: 12px 16px; - border: 1px solid var(--border-light); - border-radius: 8px; - font-size: var(--font-sm); - background: var(--bg-white); - color: var(--text-primary); - transition: border-color var(--duration-base); - min-height: 44px; + padding: var(--spacing-3) var(--spacing-4); + border: 1px solid var(--gray-300); + border-radius: var(--radius-md); + font-family: var(--font-primary); + font-size: 16px; /* iOS zoom prevention */ + background: var(--white); + transition: border-color var(--transition-normal), outline var(--transition-normal); } .form-input:focus, .form-textarea:focus, .form-select:focus { - outline: none; - border-color: var(--primary); - box-shadow: 0 0 0 3px var(--primary-light); + outline: 2px solid var(--primary-100); + border-color: var(--primary-500); } .form-input.error, .form-textarea.error, .form-select.error { border-color: var(--error); + outline: 2px solid rgba(244, 67, 54, 0.1); } .form-input:disabled, .form-textarea:disabled, .form-select:disabled { - background: var(--bg-gray); + background: var(--gray-100); + color: var(--gray-400); cursor: not-allowed; } .form-textarea { - min-height: 120px; + min-height: 100px; resize: vertical; } +.form-select { + appearance: none; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%23757575' stroke-width='2'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E"); + background-repeat: no-repeat; + background-position: right 12px center; + background-size: 20px; + padding-right: 40px; +} + .form-error { - display: none; + margin-top: var(--spacing-1); + font-size: 12px; color: var(--error); - font-size: var(--font-xs); - margin-top: var(--space-2); } -.form-group.has-error .form-error { - display: block; +/* Checkbox */ +.form-checkbox { + display: inline-flex; + align-items: center; + gap: var(--spacing-2); + cursor: pointer; + user-select: none; } -.form-hint { - font-size: var(--font-xs); - color: var(--text-secondary); - margin-top: var(--space-2); +.form-checkbox input[type="checkbox"] { + width: 20px; + height: 20px; + border: 2px solid var(--gray-400); + border-radius: var(--radius-sm); + cursor: pointer; + appearance: none; + background: var(--white); + position: relative; } -/* ========== Cards ========== */ +.form-checkbox input[type="checkbox"]:checked { + background: var(--primary-500); + border-color: var(--primary-500); +} + +.form-checkbox input[type="checkbox"]:checked::after { + content: '✓'; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + color: var(--white); + font-size: 14px; + font-weight: 700; +} + +/* ======================================== + 6. Card Styles + ======================================== */ .card { - background: var(--bg-white); - border-radius: 12px; - padding: var(--space-6); + background: var(--white); + border-radius: var(--radius-lg); + padding: var(--spacing-6); box-shadow: var(--shadow-sm); - transition: all var(--duration-base); + transition: box-shadow var(--transition-normal); } -.card-hover { +.card:hover { + box-shadow: var(--shadow-md); +} + +.card.clickable { cursor: pointer; } -.card-hover:hover { - box-shadow: var(--shadow-md); - transform: translateY(-2px); +.card-header { + margin-bottom: var(--spacing-4); } .card-title { - font-size: var(--font-lg); - font-weight: var(--font-semibold); - margin-bottom: var(--space-3); + font-size: 18px; + font-weight: 600; + color: var(--gray-900); + margin-bottom: var(--spacing-2); } .card-subtitle { - font-size: var(--font-sm); - color: var(--text-secondary); - margin-bottom: var(--space-4); + font-size: 14px; + color: var(--gray-600); } -/* ========== Badge & Chip ========== */ +.card-body { + margin-bottom: var(--spacing-4); +} + +.card-footer { + display: flex; + justify-content: flex-end; + gap: var(--spacing-2); + padding-top: var(--spacing-4); + border-top: 1px solid var(--gray-200); +} + +/* ======================================== + 7. Badge Styles + ======================================== */ .badge { display: inline-flex; align-items: center; + gap: var(--spacing-1); + padding: var(--spacing-1) var(--spacing-3); + border-radius: var(--radius-full); + font-size: 12px; + font-weight: 500; + white-space: nowrap; +} + +.badge-status { padding: 4px 12px; - border-radius: 16px; - font-size: var(--font-xs); - font-weight: var(--font-medium); } -.badge-success { - background: var(--success); - color: var(--text-inverse); +.badge-draft { + background: var(--warning-bg); + color: #E65100; } -.badge-error { - background: var(--error); - color: var(--text-inverse); +.badge-confirmed { + background: var(--success-bg); + color: #2E7D32; } -.badge-warning { - background: var(--warning); - color: var(--text-inverse); +.badge-shared { + background: var(--info-bg); + color: var(--primary-700); } -.badge-info { - background: var(--primary-light); - color: var(--primary); -} - -.chip { - display: inline-flex; - align-items: center; - padding: 6px 12px; - border-radius: 20px; - background: var(--bg-gray); - font-size: var(--font-sm); - gap: var(--space-2); -} - -.chip-remove { - background: none; - border: none; - color: var(--text-secondary); - font-size: var(--font-lg); - cursor: pointer; - padding: 0; - width: 20px; +.badge-count { + min-width: 20px; height: 20px; + padding: 0 6px; + background: var(--error); + color: var(--white); + border-radius: var(--radius-full); + font-size: 11px; + font-weight: 600; display: flex; align-items: center; justify-content: center; } -/* ========== Toast ========== */ +.badge-priority-high { + background: var(--error-bg); + color: var(--error); +} + +.badge-priority-medium { + background: var(--warning-bg); + color: #E65100; +} + +.badge-priority-low { + background: var(--gray-100); + color: var(--gray-700); +} + +/* ======================================== + 8. Modal Styles + ======================================== */ +.modal-overlay { + display: none; + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.5); + z-index: var(--z-modal-backdrop); + align-items: center; + justify-content: center; +} + +.modal-overlay.active { + display: flex; +} + +.modal-container { + background: var(--white); + border-radius: var(--radius-xl); + max-width: 500px; + width: calc(100% - 32px); + max-height: 90vh; + overflow-y: auto; + box-shadow: var(--shadow-lg); +} + +.modal-header { + padding: var(--spacing-6); + border-bottom: 1px solid var(--gray-200); + display: flex; + justify-content: space-between; + align-items: center; +} + +.modal-title { + font-size: 20px; + font-weight: 600; + color: var(--gray-900); +} + +.modal-close { + background: transparent; + border: none; + font-size: 24px; + color: var(--gray-600); + cursor: pointer; + padding: var(--spacing-2); + line-height: 1; +} + +.modal-body { + padding: var(--spacing-6); +} + +.modal-footer { + padding: var(--spacing-6); + border-top: 1px solid var(--gray-200); + display: flex; + justify-content: flex-end; + gap: var(--spacing-2); +} + +/* Mobile: Full Screen Modal */ +@media (max-width: 767px) { + .modal-container { + width: 100%; + height: 100%; + max-height: 100vh; + border-radius: 0; + } +} + +/* ======================================== + 9. Toast / Snackbar + ======================================== */ .toast { position: fixed; - bottom: 100px; + bottom: var(--spacing-6); left: 50%; transform: translateX(-50%); - background: var(--bg-dark); - color: var(--text-inverse); - padding: var(--space-4) var(--space-6); - border-radius: 8px; - box-shadow: var(--shadow-lg); - z-index: 1000; - animation: slideUp var(--duration-slow) var(--ease-in-out); + background: var(--gray-800); + color: var(--white); + padding: var(--spacing-3) var(--spacing-6); + border-radius: var(--radius-md); + box-shadow: var(--shadow-md); + z-index: var(--z-toast); + min-width: 250px; + text-align: center; + font-size: 14px; + animation: slideUp 0.3s ease-out; + display: none; +} + +.toast.active { + display: block; +} + +.toast.success { + background: var(--success); +} + +.toast.error { + background: var(--error); +} + +.toast.warning { + background: var(--warning); } @keyframes slideUp { from { + transform: translate(-50%, 20px); opacity: 0; - transform: translateX(-50%) translateY(20px); } to { + transform: translate(-50%, 0); opacity: 1; - transform: translateX(-50%) translateY(0); } } -.toast-success { background: var(--success); } -.toast-error { background: var(--error); } -.toast-warning { background: var(--warning); } - -/* ========== Modal ========== */ -.modal-overlay { - position: fixed; - top: 0; - left: 0; - right: 0; - bottom: 0; - background: rgba(0,0,0,0.5); +/* ======================================== + 10. Layout Components + ======================================== */ +.page { + min-height: 100vh; display: flex; - align-items: center; - justify-content: center; - z-index: 1000; - padding: var(--space-4); + flex-direction: column; } -.modal { - background: var(--bg-white); - border-radius: 12px; - padding: var(--space-6); - max-width: 500px; - width: 100%; - max-height: 90vh; +.header { + background: var(--white); + border-bottom: 1px solid var(--gray-200); + padding: var(--spacing-4); + display: flex; + align-items: center; + justify-content: space-between; + position: sticky; + top: 0; + z-index: var(--z-sticky); +} + +.header-title { + font-size: 18px; + font-weight: 600; + color: var(--gray-900); +} + +.content { + flex: 1; + padding: var(--spacing-4); overflow-y: auto; } -.modal-header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: var(--space-4); +.footer { + background: var(--white); + border-top: 1px solid var(--gray-200); + padding: var(--spacing-4); } -.modal-title { - font-size: var(--font-xl); - font-weight: var(--font-semibold); - margin: 0; -} - -.modal-close { - background: none; - border: none; - font-size: var(--font-2xl); - color: var(--text-secondary); - cursor: pointer; -} - -.modal-footer { - display: flex; - gap: var(--space-3); - margin-top: var(--space-6); - justify-content: flex-end; -} - -/* ========== Loading ========== */ -.loading { - display: inline-block; - width: 40px; - height: 40px; - border: 4px solid var(--border-light); - border-top-color: var(--primary); - border-radius: 50%; - animation: spin 1s linear infinite; -} - -@keyframes spin { - to { transform: rotate(360deg); } -} - -.loading-overlay { - position: fixed; - top: 0; - left: 0; - right: 0; - bottom: 0; - background: rgba(255,255,255,0.9); - display: flex; - align-items: center; - justify-content: center; - z-index: 2000; -} - -/* ========== Bottom Navigation (Mobile) ========== */ +/* Bottom Navigation */ .bottom-nav { position: fixed; bottom: 0; left: 0; right: 0; - background: var(--bg-white); - box-shadow: 0 -2px 10px rgba(0,0,0,0.1); + background: var(--white); + border-top: 1px solid var(--gray-200); display: flex; justify-content: space-around; - padding: var(--space-2) 0; - z-index: 100; -} - -@media (min-width: 768px) { - .bottom-nav { display: none; } + padding: var(--spacing-2) 0; + z-index: var(--z-fixed); } .bottom-nav-item { display: flex; flex-direction: column; align-items: center; - padding: var(--space-2); - color: var(--text-secondary); + gap: var(--spacing-1); + padding: var(--spacing-2); + color: var(--gray-600); text-decoration: none; - font-size: var(--font-xs); + font-size: 12px; min-width: 60px; - transition: color var(--duration-base); + transition: color var(--transition-normal); } .bottom-nav-item.active { - color: var(--primary); + color: var(--primary-500); } .bottom-nav-icon { - font-size: var(--font-2xl); - margin-bottom: var(--space-1); + font-size: 24px; } -/* ========== FAB (Floating Action Button) ========== */ -.fab { - position: fixed; - bottom: 100px; - right: var(--space-4); - width: 56px; - height: 56px; - border-radius: 50%; - background: var(--primary); - color: var(--text-inverse); - font-size: var(--font-2xl); - box-shadow: var(--shadow-lg); - display: flex; - align-items: center; - justify-content: center; - cursor: pointer; - border: none; - transition: all var(--duration-base); - z-index: 50; -} +/* ======================================== + 11. Utility Classes + ======================================== */ +/* Display */ +.d-none { display: none !important; } +.d-block { display: block !important; } +.d-flex { display: flex !important; } +.d-grid { display: grid !important; } -.fab:hover { - background: var(--primary-dark); - transform: scale(1.1); -} +/* Flex */ +.flex-row { flex-direction: row; } +.flex-column { flex-direction: column; } +.align-center { align-items: center; } +.align-start { align-items: flex-start; } +.align-end { align-items: flex-end; } +.justify-center { justify-content: center; } +.justify-between { justify-content: space-between; } +.justify-end { justify-content: flex-end; } +.gap-1 { gap: var(--spacing-1); } +.gap-2 { gap: var(--spacing-2); } +.gap-3 { gap: var(--spacing-3); } +.gap-4 { gap: var(--spacing-4); } +.gap-6 { gap: var(--spacing-6); } +/* Spacing */ +.m-0 { margin: 0; } +.m-1 { margin: var(--spacing-1); } +.m-2 { margin: var(--spacing-2); } +.m-3 { margin: var(--spacing-3); } +.m-4 { margin: var(--spacing-4); } +.m-6 { margin: var(--spacing-6); } +.mt-2 { margin-top: var(--spacing-2); } +.mt-4 { margin-top: var(--spacing-4); } +.mb-2 { margin-bottom: var(--spacing-2); } +.mb-4 { margin-bottom: var(--spacing-4); } +.mb-6 { margin-bottom: var(--spacing-6); } + +.p-0 { padding: 0; } +.p-2 { padding: var(--spacing-2); } +.p-4 { padding: var(--spacing-4); } +.p-6 { padding: var(--spacing-6); } + +/* Text */ +.text-center { text-align: center; } +.text-left { text-align: left; } +.text-right { text-align: right; } +.text-primary { color: var(--primary-500); } +.text-success { color: var(--success); } +.text-error { color: var(--error); } +.text-gray { color: var(--gray-600); } + +/* Background */ +.bg-white { background: var(--white); } +.bg-gray { background: var(--gray-50); } +.bg-primary { background: var(--primary-500); color: var(--white); } + +/* Width */ +.w-full { width: 100%; } +.w-auto { width: auto; } + +/* Cursor */ +.cursor-pointer { cursor: pointer; } + +/* ======================================== + 12. Responsive Breakpoints + ======================================== */ +/* Mobile: 320px ~ 767px (default) */ + +/* Tablet: 768px ~ 1023px */ @media (min-width: 768px) { - .fab { - bottom: var(--space-6); + .content { + padding: var(--spacing-6); + } + + .text-h1 { + font-size: 32px; + } + + .bottom-nav { + display: none; /* Use sidebar instead */ } } -/* ========== Avatar ========== */ -.avatar { - display: inline-block; +/* Desktop: 1024px+ */ +@media (min-width: 1024px) { + .content { + padding: var(--spacing-8); + } + + .text-body-lg { + font-size: 18px; + } + + .text-body { + font-size: 16px; + } +} + +/* ======================================== + 13. Animations + ======================================== */ +@keyframes fadeIn { + from { opacity: 0; } + to { opacity: 1; } +} + +@keyframes spin { + to { transform: rotate(360deg); } +} + +@keyframes pulse { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.5; } +} + +/* Loading Spinner */ +.spinner { width: 40px; height: 40px; + border: 4px solid var(--gray-200); + border-top-color: var(--primary-500); border-radius: 50%; - background: var(--primary-light); - color: var(--primary); + animation: spin 1s linear infinite; +} + +.spinner-sm { + width: 20px; + height: 20px; + border-width: 2px; +} + +/* ======================================== + 14. Service Specific Components + ======================================== */ +/* 회의록 리스트 아이템 */ +.meeting-item { display: flex; align-items: center; - justify-content: center; - font-weight: var(--font-semibold); - font-size: var(--font-sm); + gap: var(--spacing-3); + padding: var(--spacing-4); + background: var(--white); + border-radius: var(--radius-md); + margin-bottom: var(--spacing-2); + cursor: pointer; + transition: background var(--transition-normal); } -.avatar-sm { width: 32px; height: 32px; font-size: var(--font-xs); } -.avatar-lg { width: 56px; height: 56px; font-size: var(--font-lg); } +.meeting-item:hover { + background: var(--gray-50); +} -.avatar-group { +/* Todo 아이템 */ +.todo-item { + display: flex; + align-items: flex-start; + gap: var(--spacing-3); + padding: var(--spacing-4); + background: var(--white); + border-radius: var(--radius-md); + border-left: 4px solid transparent; + margin-bottom: var(--spacing-2); + transition: border-color var(--transition-normal); +} + +.todo-item.completed { + opacity: 0.6; + border-left-color: var(--success); +} + +.todo-item.overdue { + border-left-color: var(--error); +} + +.todo-item.due-soon { + border-left-color: var(--warning); +} + +/* 전문용어 하이라이트 */ +.term-highlight { + background: linear-gradient(180deg, transparent 60%, var(--accent-200) 60%); + cursor: pointer; + border-bottom: 1px dotted var(--accent-500); + transition: background var(--transition-normal); +} + +.term-highlight:hover { + background: var(--accent-100); +} + +/* AI 처리 인디케이터 */ +.ai-processing { display: flex; align-items: center; + gap: var(--spacing-2); + padding: var(--spacing-3) var(--spacing-4); + background: var(--accent-50); + border-radius: var(--radius-md); + border-left: 4px solid var(--accent-500); + font-size: 13px; + color: var(--accent-700); } -.avatar-group .avatar { - margin-left: -8px; - border: 2px solid var(--bg-white); +.ai-icon { + width: 20px; + height: 20px; + animation: spin 2s linear infinite; } -.avatar-group .avatar:first-child { - margin-left: 0; +/* 검증 완료 배지 */ +.verified-badge { + display: inline-flex; + align-items: center; + gap: var(--spacing-1); + background: var(--success); + color: var(--white); + padding: 4px 12px; + border-radius: var(--radius-full); + font-size: 12px; + font-weight: 600; } -/* ========== Empty State ========== */ -.empty-state { - text-align: center; - padding: var(--space-12) var(--space-4); - color: var(--text-secondary); +/* ======================================== + 15. Accessibility + ======================================== */ +*:focus { + outline: 2px solid var(--primary-500); + outline-offset: 2px; } -.empty-state-icon { - font-size: 64px; - margin-bottom: var(--space-4); - opacity: 0.5; +*:focus:not(:focus-visible) { + outline: none; } -.empty-state-title { - font-size: var(--font-xl); - color: var(--text-primary); - margin-bottom: var(--space-2); -} - -.empty-state-description { - font-size: var(--font-sm); - margin-bottom: var(--space-6); -} - -/* ========== Accessibility ========== */ .sr-only { position: absolute; width: 1px; @@ -668,52 +1000,7 @@ input, textarea, select { padding: 0; margin: -1px; overflow: hidden; - clip: rect(0,0,0,0); + clip: rect(0, 0, 0, 0); white-space: nowrap; border-width: 0; } - -*:focus-visible { - outline: 2px solid var(--primary); - outline-offset: 2px; -} - -/* Reduced Motion */ -@media (prefers-reduced-motion: reduce) { - *, *::before, *::after { - animation-duration: 0.01ms !important; - animation-iteration-count: 1 !important; - transition-duration: 0.01ms !important; - } -} - -/* ========== Utility Classes ========== */ -.text-center { text-align: center; } -.text-left { text-align: left; } -.text-right { text-align: right; } -.text-primary { color: var(--text-primary); } -.text-secondary { color: var(--text-secondary); } -.text-success { color: var(--success); } -.text-error { color: var(--error); } -.text-warning { color: var(--warning); } - -.mt-0 { margin-top: 0; } -.mt-2 { margin-top: var(--space-2); } -.mt-4 { margin-top: var(--space-4); } -.mt-6 { margin-top: var(--space-6); } -.mt-8 { margin-top: var(--space-8); } -.mb-0 { margin-bottom: 0; } -.mb-2 { margin-bottom: var(--space-2); } -.mb-4 { margin-bottom: var(--space-4); } -.mb-6 { margin-bottom: var(--space-6); } -.mb-8 { margin-bottom: var(--space-8); } - -.hidden { display: none; } -.flex { display: flex; } -.flex-col { flex-direction: column; } -.items-center { align-items: center; } -.justify-center { justify-content: center; } -.justify-between { justify-content: space-between; } -.gap-2 { gap: var(--space-2); } -.gap-4 { gap: var(--space-4); } -.gap-6 { gap: var(--space-6); } diff --git a/design/uiux_다람지/prototype/common.js b/design/uiux_다람지/prototype/common.js index e6ceb8e..f1b70db 100644 --- a/design/uiux_다람지/prototype/common.js +++ b/design/uiux_다람지/prototype/common.js @@ -1,148 +1,244 @@ -// 회의록 작성 서비스 - 공통 JavaScript +/** + * 회의록 작성 및 공유 개선 서비스 - 공통 JavaScript + * Mobile First Design + * 작성일: 2025-10-21 + */ -// ========== Mock Data ========== -const MockData = { - user: { - id: 'user-001', - username: 'kimmin', - name: '김민준', - email: 'kimmin@example.com', - role: 'user' +/* ======================================== + 1. 전역 설정 및 상수 + ======================================== */ +const APP_CONFIG = { + APP_NAME: '회의록 작성 및 공유 개선 서비스', + STORAGE_KEYS: { + USER: 'current_user', + MEETINGS: 'meetings_data', + TODOS: 'todos_data', + TEMPLATES: 'templates_data', + INITIALIZED: 'app_initialized' }, - - meetings: [ - { - id: 'meeting-001', - title: '주간 회의', - startTime: '2025-01-20T14:00:00Z', - endTime: '2025-01-20T15:00:00Z', - location: '회의실 A', - attendees: ['김민준', '박서연', '이준호', '최유진', '정도현'], - attendeesCount: 5, - status: 'scheduled' - }, - { - id: 'meeting-002', - title: 'Q1 기획 회의', - startTime: '2025-01-20T16:00:00Z', - endTime: '2025-01-20T17:00:00Z', - location: '회의실 B', - attendees: ['김민준', '박서연', '이준호'], - attendeesCount: 3, - status: 'scheduled' - } - ], - - minutes: [ - { - id: 'minutes-001', - meetingId: 'meeting-001', - title: 'Q4 기획 회의', - date: '2025-01-15', - attendees: ['김민준', '박서연', '이준호'], - content: '# 참석자\n- 김민준\n- 박서연\n- 이준호\n\n# 논의 내용\n...', - status: 'completed' - }, - { - id: 'minutes-002', - meetingId: 'meeting-002', - title: '개발팀 스크럼', - date: '2025-01-14', - attendees: ['이준호', '최유진'], - content: '# 어제 한 일\n...', - status: 'completed' - } - ], - - todos: [ - { - id: 'todo-001', - content: 'API 명세서 작성', - assignee: '이준호', - dueDate: '2025-01-25', - status: 'inprogress', - progress: 60, - minutesId: 'minutes-001' - }, - { - id: 'todo-002', - content: 'UI 프로토타입 완성', - assignee: '최유진', - dueDate: '2025-01-23', - status: 'inprogress', - progress: 80, - minutesId: 'minutes-001' - }, - { - id: 'todo-003', - content: '테스트 케이스 작성', - assignee: '정도현', - dueDate: '2025-01-22', - status: 'completed', - progress: 100, - minutesId: 'minutes-002' - } - ], - - templates: [ - { - id: 'general', - name: '일반 회의', - description: '참석자, 안건, 논의, 결정, Todo', - sections: [ - { id: 'attendees', name: '참석자', order: 1, required: true }, - { id: 'agenda', name: '안건', order: 2, required: false }, - { id: 'discussion', name: '논의 내용', order: 3, required: true }, - { id: 'decisions', name: '결정 사항', order: 4, required: true }, - { id: 'todos', name: 'Todo', order: 5, required: false } - ] - }, - { - id: 'scrum', - name: '스크럼 회의', - description: '어제, 오늘, 이슈', - sections: [ - { id: 'yesterday', name: '어제 한 일', order: 1, required: true }, - { id: 'today', name: '오늘 할 일', order: 2, required: true }, - { id: 'issues', name: '이슈', order: 3, required: false } - ] - }, - { - id: 'kickoff', - name: '프로젝트 킥오프', - description: '개요, 목표, 일정, 역할', - sections: [ - { id: 'overview', name: '프로젝트 개요', order: 1, required: true }, - { id: 'goals', name: '목표', order: 2, required: true }, - { id: 'schedule', name: '일정', order: 3, required: true }, - { id: 'roles', name: '역할', order: 4, required: true }, - { id: 'risks', name: '리스크', order: 5, required: false } - ] - }, - { - id: 'weekly', - name: '주간 회의', - description: '실적, 이슈, 계획', - sections: [ - { id: 'achievements', name: '주간 실적', order: 1, required: true }, - { id: 'issues', name: '주요 이슈', order: 2, required: false }, - { id: 'plan', name: '다음 주 계획', order: 3, required: true } - ] - } - ] + ROUTES: { + LOGIN: '01-로그인.html', + DASHBOARD: '02-대시보드.html', + MEETING_SCHEDULE: '03-회의예약.html', + TEMPLATE_SELECT: '04-템플릿선택.html', + MEETING_IN_PROGRESS: '05-회의진행.html', + VERIFICATION: '06-검증완료.html', + MEETING_END: '07-회의종료.html', + MEETING_SHARE: '08-회의록공유.html', + TODO_MANAGE: '09-Todo관리.html', + MEETING_DETAIL: '10-회의록상세조회.html', + MEETING_EDIT: '11-회의록수정.html' + } }; -// ========== Storage Utilities ========== -const Storage = { - set(key, value) { - try { - localStorage.setItem(key, JSON.stringify(value)); - } catch (e) { - console.error('Storage set error:', e); - } +// 더미 사용자 데이터 +const DUMMY_USERS = [ + { id: 'EMP001', password: '1234', name: '김철수', email: 'kim@company.com', role: '기획팀', position: '팀장' }, + { id: 'EMP002', password: '1234', name: '이영희', email: 'lee@company.com', role: '개발팀', position: '선임' }, + { id: 'EMP003', password: '1234', name: '박민수', email: 'park@company.com', role: '디자인팀', position: '사원' }, + { id: 'EMP004', password: '1234', name: '정수진', email: 'jung@company.com', role: '기획팀', position: '사원' }, + { id: 'EMP005', password: '1234', name: '최동욱', email: 'choi@company.com', role: '개발팀', position: '팀장' } +]; + +// 템플릿 데이터 +const TEMPLATES = { + general: { + id: 'TPL001', + name: '일반 회의', + type: 'general', + icon: '📝', + description: '참석자, 안건, 논의 내용, 결정 사항, Todo', + sections: [ + { id: 'SEC_participants', name: '참석자', order: 1, content: '' }, + { id: 'SEC_agenda', name: '안건', order: 2, content: '' }, + { id: 'SEC_discussion', name: '논의 내용', order: 3, content: '' }, + { id: 'SEC_decisions', name: '결정 사항', order: 4, content: '' }, + { id: 'SEC_todos', name: 'Todo', order: 5, content: '' } + ], + isDefault: true + }, + scrum: { + id: 'TPL002', + name: '스크럼 회의', + type: 'scrum', + icon: '🏃', + description: '어제 한 일, 오늘 할 일, 이슈', + sections: [ + { id: 'SEC_yesterday', name: '어제 한 일', order: 1, content: '' }, + { id: 'SEC_today', name: '오늘 할 일', order: 2, content: '' }, + { id: 'SEC_issues', name: '이슈', order: 3, content: '' } + ], + isDefault: false + }, + kickoff: { + id: 'TPL003', + name: '프로젝트 킥오프', + type: 'kickoff', + icon: '🚀', + description: '프로젝트 개요, 목표, 일정, 역할, 리스크', + sections: [ + { id: 'SEC_overview', name: '프로젝트 개요', order: 1, content: '' }, + { id: 'SEC_goals', name: '목표', order: 2, content: '' }, + { id: 'SEC_schedule', name: '일정', order: 3, content: '' }, + { id: 'SEC_roles', name: '역할', order: 4, content: '' }, + { id: 'SEC_risks', name: '리스크', order: 5, content: '' } + ], + isDefault: false + }, + weekly: { + id: 'TPL004', + name: '주간 회의', + type: 'weekly', + icon: '📅', + description: '주간 실적, 주요 이슈, 다음 주 계획', + sections: [ + { id: 'SEC_performance', name: '주간 실적', order: 1, content: '' }, + { id: 'SEC_issues', name: '주요 이슈', order: 2, content: '' }, + { id: 'SEC_next_week', name: '다음 주 계획', order: 3, content: '' } + ], + isDefault: false + } +}; + +/* ======================================== + 2. 유틸리티 함수 + ======================================== */ +const Utils = { + // 고유 ID 생성 + generateId: (prefix = 'ID') => { + return `${prefix}_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; }, - get(key) { + // 날짜 포맷팅 + formatDate: (date, format = 'YYYY-MM-DD') => { + if (!date) return ''; + const d = new Date(date); + if (isNaN(d.getTime())) return ''; + + const year = d.getFullYear(); + const month = String(d.getMonth() + 1).padStart(2, '0'); + const day = String(d.getDate()).padStart(2, '0'); + const hours = String(d.getHours()).padStart(2, '0'); + const minutes = String(d.getMinutes()).padStart(2, '0'); + + const formats = { + 'YYYY-MM-DD': `${year}-${month}-${day}`, + 'YYYY.MM.DD': `${year}.${month}.${day}`, + 'MM/DD': `${month}/${day}`, + 'YYYY-MM-DD HH:mm': `${year}-${month}-${day} ${hours}:${minutes}`, + 'HH:mm': `${hours}:${minutes}` + }; + + return formats[format] || formats['YYYY-MM-DD']; + }, + + // 상대 시간 포맷팅 + formatTimeAgo: (date) => { + if (!date) return ''; + const now = new Date(); + const past = new Date(date); + const diffMs = now - past; + const diffMinutes = Math.floor(diffMs / 60000); + const diffHours = Math.floor(diffMs / 3600000); + const diffDays = Math.floor(diffMs / 86400000); + + if (diffMinutes < 1) return '방금 전'; + if (diffMinutes < 60) return `${diffMinutes}분 전`; + if (diffHours < 24) return `${diffHours}시간 전`; + if (diffDays < 7) return `${diffDays}일 전`; + return Utils.formatDate(date); + }, + + // 경과 시간 포맷팅 (milliseconds → HH:mm:ss) + formatDuration: (ms) => { + const totalSeconds = Math.floor(ms / 1000); + const hours = Math.floor(totalSeconds / 3600); + const minutes = Math.floor((totalSeconds % 3600) / 60); + const seconds = totalSeconds % 60; + + return `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`; + }, + + // 시간 포맷팅 (HH:mm → 분 단위) + timeToMinutes: (timeString) => { + const [hours, minutes] = timeString.split(':').map(Number); + return hours * 60 + minutes; + }, + + // 문자열 자르기 + truncateText: (text, maxLength) => { + if (!text || text.length <= maxLength) return text; + return text.substring(0, maxLength) + '...'; + }, + + // 이메일 검증 + isValidEmail: (email) => { + const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + return regex.test(email); + }, + + // 사번 검증 (EMP + 3자리 숫자) + isValidEmployeeId: (id) => { + const regex = /^EMP\d{3}$/; + return regex.test(id); + }, + + // DOM 헬퍼 + $: (selector) => document.querySelector(selector), + $$: (selector) => document.querySelectorAll(selector), + + // 엘리먼트 생성 + createElement: (tag, className = '', attributes = {}) => { + const element = document.createElement(tag); + if (className) element.className = className; + Object.entries(attributes).forEach(([key, value]) => { + element.setAttribute(key, value); + }); + return element; + }, + + // 디바운스 + debounce: (func, wait = 300) => { + let timeout; + return function executedFunction(...args) { + const later = () => { + clearTimeout(timeout); + func(...args); + }; + clearTimeout(timeout); + timeout = setTimeout(later, wait); + }; + }, + + // 스로틀 + throttle: (func, limit = 100) => { + let inThrottle; + return function(...args) { + if (!inThrottle) { + func.apply(this, args); + inThrottle = true; + setTimeout(() => inThrottle = false, limit); + } + }; + }, + + // 배열 섞기 + shuffleArray: (array) => { + const newArray = [...array]; + for (let i = newArray.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [newArray[i], newArray[j]] = [newArray[j], newArray[i]]; + } + return newArray; + } +}; + +/* ======================================== + 3. 로컬 스토리지 관리 + ======================================== */ +const StorageManager = { + // 기본 CRUD + get: (key) => { try { const item = localStorage.getItem(key); return item ? JSON.parse(item) : null; @@ -152,405 +248,743 @@ const Storage = { } }, - remove(key) { + set: (key, value) => { + try { + localStorage.setItem(key, JSON.stringify(value)); + return true; + } catch (e) { + console.error('Storage set error:', e); + return false; + } + }, + + remove: (key) => { try { localStorage.removeItem(key); + return true; } catch (e) { console.error('Storage remove error:', e); + return false; } }, - clear() { + clear: () => { try { localStorage.clear(); + return true; } catch (e) { console.error('Storage clear error:', e); - } - } -}; - -// ========== Navigation ========== -const Navigation = { - goTo(page) { - window.location.href = page; - }, - - goBack() { - window.history.back(); - }, - - reload() { - window.location.reload(); - } -}; - -// ========== Toast Notifications ========== -const Toast = { - show(message, type = 'info', duration = 3000) { - const toast = document.createElement('div'); - toast.className = `toast toast-${type}`; - toast.textContent = message; - toast.setAttribute('role', 'alert'); - toast.setAttribute('aria-live', 'assertive'); - - document.body.appendChild(toast); - - setTimeout(() => { - toast.remove(); - }, duration); - }, - - success(message) { - this.show(message, 'success'); - }, - - error(message) { - this.show(message, 'error'); - }, - - warning(message) { - this.show(message, 'warning'); - }, - - info(message) { - this.show(message, 'info'); - } -}; - -// ========== Modal ========== -const Modal = { - show(options) { - const { title, content, buttons = [] } = options; - - const overlay = document.createElement('div'); - overlay.className = 'modal-overlay'; - overlay.setAttribute('role', 'dialog'); - overlay.setAttribute('aria-modal', 'true'); - overlay.setAttribute('aria-labelledby', 'modal-title'); - - const modal = document.createElement('div'); - modal.className = 'modal'; - - const header = document.createElement('div'); - header.className = 'modal-header'; - header.innerHTML = ` - - - `; - - const body = document.createElement('div'); - body.className = 'modal-body'; - body.innerHTML = content; - - const footer = document.createElement('div'); - footer.className = 'modal-footer'; - buttons.forEach(btn => { - const button = document.createElement('button'); - button.className = `btn ${btn.className || 'btn-secondary'}`; - button.textContent = btn.text; - button.onclick = () => { - if (btn.onClick) btn.onClick(); - this.close(overlay); - }; - footer.appendChild(button); - }); - - modal.appendChild(header); - modal.appendChild(body); - if (buttons.length > 0) modal.appendChild(footer); - overlay.appendChild(modal); - document.body.appendChild(overlay); - - // Close handlers - header.querySelector('.modal-close').onclick = () => this.close(overlay); - overlay.onclick = (e) => { - if (e.target === overlay) this.close(overlay); - }; - - return overlay; - }, - - close(overlay) { - if (overlay && overlay.parentElement) { - overlay.remove(); + return false; } }, - confirm(message) { - return new Promise((resolve) => { - this.show({ - title: '확인', - content: message, - buttons: [ - { - text: '취소', - className: 'btn-secondary', - onClick: () => resolve(false) - }, - { - text: '확인', - className: 'btn-primary', - onClick: () => resolve(true) - } - ] - }); - }); - } -}; - -// ========== Loading ========== -const Loading = { - show() { - const overlay = document.createElement('div'); - overlay.className = 'loading-overlay'; - overlay.id = 'loading-overlay'; - overlay.setAttribute('role', 'status'); - overlay.setAttribute('aria-live', 'polite'); - overlay.innerHTML = '
로딩 중...'; - document.body.appendChild(overlay); + // 사용자 관련 + getCurrentUser: () => { + return StorageManager.get(APP_CONFIG.STORAGE_KEYS.USER); }, - hide() { - const overlay = document.getElementById('loading-overlay'); - if (overlay) overlay.remove(); - } -}; - -// ========== Form Validation ========== -const Validator = { - required(value) { - return value && value.trim().length > 0; + setCurrentUser: (user) => { + return StorageManager.set(APP_CONFIG.STORAGE_KEYS.USER, user); }, - email(value) { - const re = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; - return re.test(value); + logout: () => { + StorageManager.remove(APP_CONFIG.STORAGE_KEYS.USER); + NavigationHelper.navigate('LOGIN'); }, - minLength(value, length) { - return value && value.length >= length; + // 회의록 관련 + getMeetings: () => { + return StorageManager.get(APP_CONFIG.STORAGE_KEYS.MEETINGS) || []; }, - maxLength(value, length) { - return value && value.length <= length; + getMeetingById: (id) => { + const meetings = StorageManager.getMeetings(); + return meetings.find(m => m.id === id); }, - validateForm(formId, rules) { - const form = document.getElementById(formId); - if (!form) return false; - - let isValid = true; - - Object.keys(rules).forEach(fieldName => { - const field = form.querySelector(`[name="${fieldName}"]`); - const fieldRules = rules[fieldName]; - const formGroup = field.closest('.form-group'); - - formGroup.classList.remove('has-error'); - - for (const rule of fieldRules) { - if (!rule.validator(field.value)) { - isValid = false; - formGroup.classList.add('has-error'); - const errorEl = formGroup.querySelector('.form-error'); - if (errorEl) errorEl.textContent = rule.message; - break; - } - } - }); - - return isValid; - } -}; - -// ========== Date/Time Utilities ========== -const DateTime = { - formatDate(dateString) { - const date = new Date(dateString); - return date.toLocaleDateString('ko-KR', { - year: 'numeric', - month: '2-digit', - day: '2-digit' - }); + addMeeting: (meeting) => { + const meetings = StorageManager.getMeetings(); + meetings.push(meeting); + return StorageManager.set(APP_CONFIG.STORAGE_KEYS.MEETINGS, meetings); }, - formatTime(dateString) { - const date = new Date(dateString); - return date.toLocaleTimeString('ko-KR', { - hour: '2-digit', - minute: '2-digit', - hour12: false - }); - }, - - formatDateTime(dateString) { - return `${this.formatDate(dateString)} ${this.formatTime(dateString)}`; - }, - - formatDuration(seconds) { - const hours = Math.floor(seconds / 3600); - const minutes = Math.floor((seconds % 3600) / 60); - const secs = seconds % 60; - return `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:${String(secs).padStart(2, '0')}`; - } -}; - -// ========== Authentication ========== -const Auth = { - login(username, password) { - // Mock login - in real app, call API - if (username === 'kimmin' && password === 'password123') { - const token = 'mock-jwt-token-' + Date.now(); - Storage.set('auth_token', token); - Storage.set('user', MockData.user); - return true; + updateMeeting: (id, updates) => { + const meetings = StorageManager.getMeetings(); + const index = meetings.findIndex(m => m.id === id); + if (index !== -1) { + meetings[index] = { ...meetings[index], ...updates, updatedAt: new Date().toISOString() }; + return StorageManager.set(APP_CONFIG.STORAGE_KEYS.MEETINGS, meetings); } return false; }, - logout() { - Storage.remove('auth_token'); - Storage.remove('user'); - Navigation.goTo('01-로그인.html'); + deleteMeeting: (id) => { + const meetings = StorageManager.getMeetings(); + const filtered = meetings.filter(m => m.id !== id); + return StorageManager.set(APP_CONFIG.STORAGE_KEYS.MEETINGS, filtered); }, - isAuthenticated() { - return !!Storage.get('auth_token'); + // Todo 관련 + getTodos: () => { + return StorageManager.get(APP_CONFIG.STORAGE_KEYS.TODOS) || []; }, - getCurrentUser() { - return Storage.get('user'); + getTodoById: (id) => { + const todos = StorageManager.getTodos(); + return todos.find(t => t.id === id); }, - requireAuth() { - if (!this.isAuthenticated()) { - Navigation.goTo('01-로그인.html'); + addTodo: (todo) => { + const todos = StorageManager.getTodos(); + todos.push(todo); + return StorageManager.set(APP_CONFIG.STORAGE_KEYS.TODOS, todos); + }, + + updateTodo: (id, updates) => { + const todos = StorageManager.getTodos(); + const index = todos.findIndex(t => t.id === id); + if (index !== -1) { + todos[index] = { ...todos[index], ...updates }; + return StorageManager.set(APP_CONFIG.STORAGE_KEYS.TODOS, todos); + } + return false; + }, + + deleteTodo: (id) => { + const todos = StorageManager.getTodos(); + const filtered = todos.filter(t => t.id !== id); + return StorageManager.set(APP_CONFIG.STORAGE_KEYS.TODOS, filtered); + }, + + // 템플릿 관련 + getTemplates: () => { + return TEMPLATES; + }, + + getTemplateById: (type) => { + return TEMPLATES[type] || TEMPLATES.general; + } +}; + +/* ======================================== + 4. 네비게이션 헬퍼 + ======================================== */ +const NavigationHelper = { + navigate: (routeKey, params = {}) => { + const route = APP_CONFIG.ROUTES[routeKey]; + if (!route) { + console.error('Invalid route:', routeKey); + return; + } + + // 파라미터를 query string으로 변환 + const queryString = Object.keys(params).length > 0 + ? '?' + Object.entries(params).map(([key, value]) => `${key}=${encodeURIComponent(value)}`).join('&') + : ''; + + window.location.href = route + queryString; + }, + + goBack: () => { + window.history.back(); + }, + + reload: () => { + window.location.reload(); + }, + + getCurrentPage: () => { + return window.location.pathname.split('/').pop(); + }, + + getQueryParams: () => { + const params = {}; + const queryString = window.location.search.substring(1); + const pairs = queryString.split('&'); + + pairs.forEach(pair => { + const [key, value] = pair.split('='); + if (key) { + params[decodeURIComponent(key)] = decodeURIComponent(value || ''); + } + }); + + return params; + }, + + getQueryParam: (key) => { + const params = NavigationHelper.getQueryParams(); + return params[key] || null; + }, + + requireAuth: () => { + const user = StorageManager.getCurrentUser(); + if (!user) { + NavigationHelper.navigate('LOGIN'); return false; } return true; + }, + + redirectToLogin: () => { + NavigationHelper.navigate('LOGIN'); } }; -// ========== API Mock ========== -const API = { - delay() { - return new Promise(resolve => setTimeout(resolve, 500)); +/* ======================================== + 5. UI 컴포넌트 생성기 + ======================================== */ +const UIComponents = { + // Toast 메시지 + showToast: (message, type = 'info', duration = 3000) => { + // 기존 toast 제거 + const existing = Utils.$('.toast'); + if (existing) existing.remove(); + + // 새 toast 생성 + const toast = Utils.createElement('div', `toast ${type} active`); + toast.textContent = message; + document.body.appendChild(toast); + + // 자동 제거 + setTimeout(() => { + toast.classList.remove('active'); + setTimeout(() => toast.remove(), 300); + }, duration); }, - async getMeetings() { - await this.delay(); - return { success: true, data: MockData.meetings }; + // 로딩 인디케이터 + showLoading: (message = '로딩 중...') => { + const existing = Utils.$('#loading-overlay'); + if (existing) return; + + const overlay = Utils.createElement('div', 'modal-overlay active', { id: 'loading-overlay' }); + overlay.innerHTML = ` +
+
+

${message}

+
+ `; + document.body.appendChild(overlay); }, - async getMinutes() { - await this.delay(); - return { success: true, data: MockData.minutes }; + hideLoading: () => { + const overlay = Utils.$('#loading-overlay'); + if (overlay) overlay.remove(); }, - async getTodos() { - await this.delay(); - const todos = MockData.todos; - const summary = { - inProgress: todos.filter(t => t.status === 'inprogress').length, - completed: todos.filter(t => t.status === 'completed').length, - total: todos.length + // 확인 다이얼로그 + confirm: (message, onConfirm, onCancel) => { + const overlay = Utils.createElement('div', 'modal-overlay active'); + overlay.innerHTML = ` + + `; + document.body.appendChild(overlay); + + Utils.$('#modal-confirm').addEventListener('click', () => { + overlay.remove(); + if (onConfirm) onConfirm(); + }); + + Utils.$('#modal-cancel').addEventListener('click', () => { + overlay.remove(); + if (onCancel) onCancel(); + }); + + overlay.addEventListener('click', (e) => { + if (e.target === overlay) { + overlay.remove(); + if (onCancel) onCancel(); + } + }); + }, + + // 모달 표시 + showModal: (options) => { + const { title, content, footer, onClose } = options; + + const overlay = Utils.createElement('div', 'modal-overlay active'); + overlay.innerHTML = ` + + `; + document.body.appendChild(overlay); + + // 닫기 버튼 + Utils.$('#modal-close-btn').addEventListener('click', () => { + overlay.remove(); + if (onClose) onClose(); + }); + + // 오버레이 클릭 시 닫기 + overlay.addEventListener('click', (e) => { + if (e.target === overlay) { + overlay.remove(); + if (onClose) onClose(); + } + }); + + return overlay; + }, + + // 배지 생성 + createBadge: (text, type = 'status') => { + const badgeClass = `badge badge-${type}`; + return `${text}`; + }, + + // 아바타 생성 + createAvatar: (name, size = 40) => { + const initial = name ? name[0].toUpperCase() : '?'; + const colors = ['#2196F3', '#4CAF50', '#FF9800', '#9C27B0', '#F44336']; + const colorIndex = name ? name.charCodeAt(0) % colors.length : 0; + const bgColor = colors[colorIndex]; + + return ` +
+ ${initial} +
+ `; + }, + + // 회의록 아이템 카드 생성 + createMeetingItem: (meeting) => { + const statusText = { + 'scheduled': '예정', + 'in-progress': '진행중', + 'draft': '작성중', + 'confirmed': '확정완료' }; - return { success: true, data: { todos, summary } }; - }, - async createMeeting(data) { - await this.delay(); - const newMeeting = { - id: 'meeting-' + Date.now(), - ...data, - status: 'scheduled' + const statusClass = { + 'scheduled': 'badge-shared', + 'in-progress': 'badge-shared', + 'draft': 'badge-draft', + 'confirmed': 'badge-confirmed' }; - MockData.meetings.push(newMeeting); - return { success: true, data: newMeeting }; + + return ` +
+
+

${meeting.title}

+

${Utils.formatDate(meeting.date)} ${meeting.startTime || ''} · ${meeting.attendees?.length || 0}명

+
+
+ ${UIComponents.createBadge(statusText[meeting.status] || '작성중', statusClass[meeting.status] || 'draft')} +
+
+ `; }, - async getTemplates() { - await this.delay(); - return { success: true, data: MockData.templates }; - }, + // Todo 아이템 카드 생성 + createTodoItem: (todo) => { + const today = new Date(); + const dueDate = new Date(todo.dueDate); + const diffDays = Math.ceil((dueDate - today) / (1000 * 60 * 60 * 24)); - async updateTodo(id, updates) { - await this.delay(); - const todo = MockData.todos.find(t => t.id === id); - if (todo) { - Object.assign(todo, updates); - return { success: true, data: todo }; + let itemClass = 'todo-item'; + if (todo.completed) { + itemClass += ' completed'; + } else if (diffDays < 0) { + itemClass += ' overdue'; + } else if (diffDays <= 3) { + itemClass += ' due-soon'; } - return { success: false, error: 'Todo not found' }; + + const priorityBadge = todo.priority === 'high' + ? '높음' + : todo.priority === 'medium' + ? '보통' + : ''; + + return ` +
+ +
+

${todo.content}

+
+ 👤 ${todo.assignee} + 📅 ${Utils.formatDate(todo.dueDate)} + ${priorityBadge} +
+
+ +
+ `; + }, + + // 진행률 바 생성 + createProgressBar: (percent) => { + return ` +
+
+
+ `; + }, + + // 원형 진행률 생성 + createCircularProgress: (percent) => { + return ` +
+
+ ${Math.round(percent)}% + 완료율 +
+
+ `; } }; -// ========== UI Helpers ========== -const UI = { - showLoading() { - Loading.show(); +/* ======================================== + 6. 폼 검증 + ======================================== */ +const FormValidator = { + rules: { + required: (value) => { + return value.trim() !== ''; + }, + email: (value) => { + return Utils.isValidEmail(value); + }, + minLength: (value, min) => { + return value.length >= min; + }, + maxLength: (value, max) => { + return value.length <= max; + }, + employeeId: (value) => { + return Utils.isValidEmployeeId(value); + } }, - hideLoading() { - Loading.hide(); + messages: { + required: '필수 입력 항목입니다.', + email: '올바른 이메일 형식이 아닙니다.', + minLength: (min) => `최소 ${min}자 이상 입력해주세요.`, + maxLength: (max) => `최대 ${max}자까지 입력 가능합니다.`, + employeeId: '올바른 사번 형식이 아닙니다. (예: EMP001)' }, - showToast(message, type = 'info') { - Toast.show(message, type); + validateField: (fieldElement, ruleName, ...args) => { + const value = fieldElement.value; + const rule = FormValidator.rules[ruleName]; + + if (!rule) { + console.error('Unknown validation rule:', ruleName); + return true; + } + + const isValid = rule(value, ...args); + + if (!isValid) { + const message = typeof FormValidator.messages[ruleName] === 'function' + ? FormValidator.messages[ruleName](...args) + : FormValidator.messages[ruleName]; + FormValidator.showError(fieldElement, message); + } else { + FormValidator.clearError(fieldElement); + } + + return isValid; }, - showModal(options) { - return Modal.show(options); + validate: (formElement) => { + let isValid = true; + const fields = formElement.querySelectorAll('[data-validate]'); + + fields.forEach(field => { + const rules = field.dataset.validate.split('|'); + rules.forEach(rule => { + const [ruleName, ...args] = rule.split(':'); + if (!FormValidator.validateField(field, ruleName, ...args)) { + isValid = false; + } + }); + }); + + return isValid; }, - confirm(message) { - return Modal.confirm(message); + showError: (fieldElement, message) => { + FormValidator.clearError(fieldElement); + + fieldElement.classList.add('error'); + const errorDiv = Utils.createElement('div', 'form-error'); + errorDiv.textContent = message; + fieldElement.parentNode.appendChild(errorDiv); }, - setTitle(title) { - document.title = title + ' - 회의록 작성 서비스'; - const headerTitle = document.querySelector('.header-title'); - if (headerTitle) headerTitle.textContent = title; - }, - - createElement(tag, className, content) { - const el = document.createElement(tag); - if (className) el.className = className; - if (content) el.innerHTML = content; - return el; + clearError: (fieldElement) => { + fieldElement.classList.remove('error'); + const errorDiv = fieldElement.parentNode.querySelector('.form-error'); + if (errorDiv) errorDiv.remove(); } }; -// ========== Initialize ========== +/* ======================================== + 7. 데이터 초기화 + ======================================== */ +const DataInitializer = { + initializeSampleData: () => { + // 이미 초기화되었는지 확인 + if (StorageManager.get(APP_CONFIG.STORAGE_KEYS.INITIALIZED)) { + return; + } + + // 샘플 회의록 데이터 + const sampleMeetings = [ + { + id: 'MTG001', + title: '프로젝트 킥오프 회의', + date: '2025-10-20', + startTime: '10:00', + endTime: '11:30', + duration: 5400000, + location: '회의실 A', + attendees: ['김철수', '이영희', '박민수'], + template: 'kickoff', + status: 'confirmed', + sections: [ + { id: 'SEC001', name: '프로젝트 개요', content: '신규 회의록 서비스 개발 프로젝트 킥오프', verified: true, verifiedBy: ['김철수', '이영희'] }, + { id: 'SEC002', name: '목표', content: '2025년 Q4 런칭, Mobile First 설계', verified: true, verifiedBy: ['김철수'] }, + { id: 'SEC003', name: '일정', content: '기획 2주, 설계 3주, 개발 8주, 테스트 2주', verified: true, verifiedBy: ['이영희'] }, + { id: 'SEC004', name: '역할', content: '김철수(PM), 이영희(개발리드), 박민수(디자인)', verified: false, verifiedBy: [] }, + { id: 'SEC005', name: '리스크', content: '일정 지연 가능성, AI 모델 성능', verified: false, verifiedBy: [] } + ], + createdBy: 'EMP001', + createdAt: '2025-10-20T09:00:00Z', + updatedAt: '2025-10-20T11:30:00Z', + confirmedAt: '2025-10-20T12:00:00Z', + sharedWith: ['EMP002', 'EMP003'] + }, + { + id: 'MTG002', + title: '주간 스크럼 회의', + date: '2025-10-21', + startTime: '09:00', + endTime: '09:30', + duration: 1800000, + location: '온라인', + attendees: ['김철수', '이영희', '정수진'], + template: 'scrum', + status: 'confirmed', + sections: [ + { id: 'SEC011', name: '어제 한 일', content: 'API 설계 완료, 데이터베이스 스키마 정의', verified: true, verifiedBy: ['김철수'] }, + { id: 'SEC012', name: '오늘 할 일', content: '프론트엔드 프로토타입 개발 시작', verified: true, verifiedBy: ['이영희'] }, + { id: 'SEC013', name: '이슈', content: '외부 API 연동 지연 (해결 중)', verified: true, verifiedBy: ['정수진'] } + ], + createdBy: 'EMP001', + createdAt: '2025-10-21T08:30:00Z', + updatedAt: '2025-10-21T09:30:00Z', + confirmedAt: '2025-10-21T10:00:00Z', + sharedWith: ['EMP002', 'EMP004'] + }, + { + id: 'MTG003', + title: '디자인 리뷰 회의', + date: '2025-10-19', + startTime: '14:00', + endTime: '15:00', + duration: 3600000, + location: '회의실 B', + attendees: ['박민수', '김철수', '정수진'], + template: 'general', + status: 'draft', + sections: [ + { id: 'SEC021', name: '참석자', content: '박민수, 김철수, 정수진', verified: false, verifiedBy: [] }, + { id: 'SEC022', name: '안건', content: 'UI/UX 초안 검토', verified: false, verifiedBy: [] }, + { id: 'SEC023', name: '논의 내용', content: 'Mobile First 접근 방식 확정, 컬러 시스템 논의 중', verified: false, verifiedBy: [] }, + { id: 'SEC024', name: '결정 사항', content: '', verified: false, verifiedBy: [] }, + { id: 'SEC025', name: 'Todo', content: '', verified: false, verifiedBy: [] } + ], + createdBy: 'EMP003', + createdAt: '2025-10-19T13:30:00Z', + updatedAt: '2025-10-19T15:00:00Z', + confirmedAt: null, + sharedWith: [] + } + ]; + + // 샘플 Todo 데이터 + const sampleTodos = [ + { + id: 'TODO001', + meetingId: 'MTG001', + sectionId: 'SEC003', + content: '프로젝트 계획서 작성 및 공유', + assignee: '김철수', + assigneeId: 'EMP001', + dueDate: '2025-10-25', + priority: 'high', + status: 'in-progress', + completed: false, + completedAt: null, + createdAt: '2025-10-20T11:30:00Z' + }, + { + id: 'TODO002', + meetingId: 'MTG001', + sectionId: 'SEC004', + content: '디자인 시안 1차 검토', + assignee: '박민수', + assigneeId: 'EMP003', + dueDate: '2025-10-23', + priority: 'medium', + status: 'completed', + completed: true, + completedAt: '2025-10-22T15:00:00Z', + createdAt: '2025-10-20T11:30:00Z' + }, + { + id: 'TODO003', + meetingId: 'MTG002', + sectionId: 'SEC012', + content: 'API 문서 작성', + assignee: '이영희', + assigneeId: 'EMP002', + dueDate: '2025-10-24', + priority: 'high', + status: 'in-progress', + completed: false, + completedAt: null, + createdAt: '2025-10-21T09:30:00Z' + }, + { + id: 'TODO004', + meetingId: 'MTG001', + sectionId: 'SEC005', + content: 'AI 모델 성능 테스트', + assignee: '정수진', + assigneeId: 'EMP004', + dueDate: '2025-10-22', + priority: 'high', + status: 'overdue', + completed: false, + completedAt: null, + createdAt: '2025-10-20T11:30:00Z' + }, + { + id: 'TODO005', + meetingId: 'MTG002', + sectionId: 'SEC013', + content: '외부 API 연동 이슈 해결', + assignee: '이영희', + assigneeId: 'EMP002', + dueDate: '2025-10-26', + priority: 'medium', + status: 'in-progress', + completed: false, + completedAt: null, + createdAt: '2025-10-21T09:30:00Z' + } + ]; + + // 데이터 저장 + StorageManager.set(APP_CONFIG.STORAGE_KEYS.MEETINGS, sampleMeetings); + StorageManager.set(APP_CONFIG.STORAGE_KEYS.TODOS, sampleTodos); + StorageManager.set(APP_CONFIG.STORAGE_KEYS.INITIALIZED, true); + + console.log('Sample data initialized successfully'); + }, + + resetData: () => { + StorageManager.clear(); + DataInitializer.initializeSampleData(); + console.log('Data reset successfully'); + } +}; + +/* ======================================== + 8. 전역 이벤트 핸들러 + ======================================== */ + +// Todo 완료/미완료 토글 +function handleTodoToggle(todoId, completed) { + const todo = StorageManager.getTodoById(todoId); + if (!todo) return; + + todo.completed = completed; + todo.status = completed ? 'completed' : 'in-progress'; + todo.completedAt = completed ? new Date().toISOString() : null; + + StorageManager.updateTodo(todoId, todo); + + // 회의록의 Todo 섹션 업데이트 + const meeting = StorageManager.getMeetingById(todo.meetingId); + if (meeting) { + // 실시간 반영 시뮬레이션 + console.log(`Todo ${todoId} 완료 상태가 회의록 ${todo.meetingId}에 반영되었습니다.`); + } + + UIComponents.showToast( + completed ? 'Todo가 완료되었습니다' : 'Todo가 미완료로 변경되었습니다', + completed ? 'success' : 'info' + ); + + // 현재 페이지가 Todo 관리 화면이면 리로드 + if (NavigationHelper.getCurrentPage() === APP_CONFIG.ROUTES.TODO_MANAGE) { + setTimeout(() => window.location.reload(), 1000); + } +} + +// 마감 임박 여부 확인 (3일 이내) +function isDueSoon(dueDate) { + if (!dueDate) return false; + const today = new Date(); + const due = new Date(dueDate); + const diffDays = Math.ceil((due - today) / (1000 * 60 * 60 * 24)); + return diffDays >= 0 && diffDays <= 3; +} + +/* ======================================== + 9. 앱 초기화 + ======================================== */ document.addEventListener('DOMContentLoaded', () => { - // Back button handler - const backButtons = document.querySelectorAll('.header-back'); - backButtons.forEach(btn => { - btn.addEventListener('click', () => Navigation.goBack()); - }); + // 샘플 데이터 초기화 (최초 1회만) + DataInitializer.initializeSampleData(); - // Bottom navigation active state - const currentPage = window.location.pathname.split('/').pop(); + // 하단 네비게이션 활성화 + const currentPage = NavigationHelper.getCurrentPage(); const navItems = document.querySelectorAll('.bottom-nav-item'); + navItems.forEach(item => { const href = item.getAttribute('href'); - if (href === currentPage) { + if (href && href.includes(currentPage)) { item.classList.add('active'); } }); }); -// ========== Export for use in pages ========== -window.App = { - MockData, - Storage, - Navigation, - Toast, - Modal, - Loading, - Validator, - DateTime, - Auth, - API, - UI -}; +// 전역 함수로 노출 +window.Utils = Utils; +window.StorageManager = StorageManager; +window.NavigationHelper = NavigationHelper; +window.UIComponents = UIComponents; +window.FormValidator = FormValidator; +window.DataInitializer = DataInitializer; +window.handleTodoToggle = handleTodoToggle; +window.isDueSoon = isDueSoon; +window.TEMPLATES = TEMPLATES; +window.DUMMY_USERS = DUMMY_USERS; diff --git a/design/uiux_다람지/style-guide.md b/design/uiux_다람지/style-guide.md index f167f16..8ec6cf6 100644 --- a/design/uiux_다람지/style-guide.md +++ b/design/uiux_다람지/style-guide.md @@ -1,420 +1,1767 @@ -# 회의록 작성 서비스 - 스타일 가이드 +# 회의록 작성 및 공유 개선 서비스 - 스타일 가이드 -## 1. 디자인 시스템 개요 - -### 1.1 디자인 철학 -- **Mobile First**: 모바일 환경을 우선으로 설계하고 점진적으로 향상 -- **접근성 우선**: WCAG 2.1 Level AA 준수 -- **일관성**: 모든 화면에서 동일한 시각적 언어 사용 +## 문서 정보 +- **작성일**: 2025-10-21 +- **작성자**: 이미준 (서비스 기획자) +- **버전**: 1.0 +- **기반**: Mobile First Design Philosophy --- -## 2. 색상 팔레트 +## 목차 +1. [브랜드 아이덴티티](#브랜드-아이덴티티) +2. [디자인 원칙](#디자인-원칙) +3. [컬러 시스템](#컬러-시스템) +4. [타이포그래피](#타이포그래피) +5. [간격 시스템](#간격-시스템) +6. [컴포넌트 스타일](#컴포넌트-스타일) +7. [반응형 브레이크포인트](#반응형-브레이크포인트) +8. [서비스 특화 컴포넌트](#서비스-특화-컴포넌트) +9. [인터랙션 패턴](#인터랙션-패턴) +10. [아이콘 시스템](#아이콘-시스템) +11. [애니메이션 가이드](#애니메이션-가이드) +12. [상태 표시](#상태-표시) +13. [접근성 표준](#접근성-표준) +14. [변경 이력](#변경-이력) -### 2.1 Primary Colors +--- + +## 1. 브랜드 아이덴티티 + +### 디자인 컨셉 +**"정확하고 지능적인 협업"** + +업무 지식이 없어도 AI의 도움으로 정확한 회의록을 작성하고, 실시간으로 협업하며, 효율적으로 공유할 수 있는 전문적이면서도 접근하기 쉬운 서비스. + +### 핵심 가치 +- **정확성 (Accuracy)**: AI 기반 자동 작성과 맥락 기반 용어 설명으로 정확한 회의록 보장 +- **협업 (Collaboration)**: 실시간 동기화와 섹션별 검증으로 참석자 간 원활한 협업 +- **지능 (Intelligence)**: AI 자동 요약, Todo 추출, 관련 회의록 연결 등 스마트한 기능 +- **효율성 (Efficiency)**: Mobile First 설계로 언제 어디서나 빠르고 편리한 접근 + +### 브랜드 성격 +- **전문적 (Professional)**: 비즈니스 환경에 적합한 신뢰감 +- **접근 가능 (Accessible)**: 누구나 쉽게 사용할 수 있는 직관성 +- **스마트 (Smart)**: AI 기술의 지능적 활용 +- **협력적 (Collaborative)**: 팀워크를 촉진하는 협업 중심 + +### 디자인 키워드 +- Clean & Modern +- Intuitive & User-friendly +- Professional & Trustworthy +- Collaborative & Connected +- AI-powered & Smart + +--- + +## 2. 디자인 원칙 + +### 2.1 Mobile First +**작은 화면에서 시작하여 큰 화면으로 확장** + +- **우선순위 중심**: 제한된 공간에서 가장 중요한 기능과 정보에 집중 +- **점진적 향상**: 모바일 기본 경험을 먼저 완성하고, 화면이 커질수록 기능 추가 +- **성능 최적화**: 모바일 환경의 제약(네트워크, 처리 능력)을 우선 고려 +- **터치 우선**: 터치 인터랙션을 기본으로 설계, 마우스는 추가 기능 + +### 2.2 사용자 중심 설계 +**사용자가 목표를 쉽게 달성할 수 있도록 지원** + +- **단순함 (Simplicity)**: 복잡한 기능을 간단하게 표현 +- **일관성 (Consistency)**: 동일한 패턴과 용어를 전체 서비스에서 일관되게 사용 +- **피드백 (Feedback)**: 모든 사용자 액션에 즉각적이고 명확한 반응 제공 +- **오류 방지 (Error Prevention)**: 실수를 사전에 차단하고, 발생 시 명확한 해결 방법 제공 + +### 2.3 접근성 우선 +**모든 사용자가 동등하게 사용할 수 있도록 설계** + +- **WCAG 2.1 Level AA 준수**: 국제 웹 접근성 표준 충족 +- **키보드 네비게이션**: 모든 기능을 키보드만으로 조작 가능 +- **스크린 리더 지원**: 시각장애인을 위한 적절한 ARIA 속성 제공 +- **색상 대비**: 최소 4.5:1 대비율로 가독성 보장 + +### 2.4 실시간 협업 지원 +**여러 사용자가 동시에 작업할 수 있도록 최적화** + +- **즉각적 동기화**: 변경 사항을 실시간으로 모든 참석자에게 전달 +- **충돌 방지**: 동시 편집 충돌을 사전 감지 및 해결 +- **명확한 상태 표시**: 누가 무엇을 하고 있는지 실시간으로 표시 + +### 2.5 AI 투명성 +**AI 기능을 이해하기 쉽고 신뢰할 수 있게 표현** + +- **진행 상황 표시**: AI 처리 중임을 명확히 알림 +- **신뢰도 표시**: AI 결과의 신뢰도를 시각적으로 전달 +- **수정 가능성**: AI 결과를 언제든 수동으로 수정 가능 + +--- + +## 3. 컬러 시스템 + +### 3.1 Primary Colors (주요 색상) + +#### Blue (메인 브랜드 색상) +전문성, 신뢰, 기술을 나타내는 주요 색상 + +- **Primary 50**: `#E3F2FD` - 배경, 하이라이트 +- **Primary 100**: `#BBDEFB` - 호버 배경 +- **Primary 200**: `#90CAF9` - 비활성 상태 +- **Primary 300**: `#64B5F6` - 보조 요소 +- **Primary 400**: `#42A5F5` - 인터랙티브 요소 +- **Primary 500**: `#2196F3` ⭐ **기본** - 주요 버튼, 링크 +- **Primary 600**: `#1E88E5` - 호버 상태 +- **Primary 700**: `#1976D2` - Active 상태 +- **Primary 800**: `#1565C0` - 강조 +- **Primary 900**: `#0D47A1` - 최고 강조 + +**사용 예시:** +- 주요 액션 버튼 (회의 시작, 저장, 공유) +- 링크 텍스트 +- 선택된 탭/메뉴 +- 진행 바 +- 포커스 인디케이터 + +### 3.2 Secondary Colors (보조 색상) + +#### Green (성공, 완료) +완료 상태, 긍정적 액션을 나타냄 + +- **Secondary 50**: `#E8F5E9` +- **Secondary 100**: `#C8E6C9` +- **Secondary 500**: `#4CAF50` ⭐ **기본** +- **Secondary 700**: `#388E3C` +- **Secondary 900**: `#1B5E20` + +**사용 예시:** +- 검증 완료 배지 +- Todo 완료 체크 +- 성공 메시지 +- 진행률 표시 + +#### Purple (AI 기능) +AI 관련 기능을 시각적으로 구분 + +- **Accent 50**: `#F3E5F5` +- **Accent 100**: `#E1BEE7` +- **Accent 500**: `#9C27B0` ⭐ **기본** +- **Accent 700**: `#7B1FA2` + +**사용 예시:** +- AI 자동 작성 인디케이터 +- 전문용어 하이라이트 +- AI 제안 배지 +- 맥락 기반 설명 툴팁 + +### 3.3 Semantic Colors (의미 색상) + +#### Success (성공) +- **Color**: `#4CAF50` (Green 500) +- **Background**: `#E8F5E9` (Green 50) +- **사용**: 성공 메시지, 완료 상태 + +#### Warning (경고) +- **Color**: `#FF9800` (Orange 500) +- **Background**: `#FFF3E0` (Orange 50) +- **사용**: 경고 메시지, 주의 필요 + +#### Error (오류) +- **Color**: `#F44336` (Red 500) +- **Background**: `#FFEBEE` (Red 50) +- **사용**: 오류 메시지, 필수 항목 누락 + +#### Info (정보) +- **Color**: `#2196F3` (Blue 500) +- **Background**: `#E3F2FD` (Blue 50) +- **사용**: 정보 메시지, 안내 + +### 3.4 Neutral Colors (중립 색상) + +#### Gray Scale +텍스트, 배경, 테두리에 사용 + +- **Gray 50**: `#FAFAFA` - 페이지 배경 +- **Gray 100**: `#F5F5F5` - 카드 배경 +- **Gray 200**: `#EEEEEE` - 비활성 배경 +- **Gray 300**: `#E0E0E0` - 테두리 +- **Gray 400**: `#BDBDBD` - 비활성 텍스트 +- **Gray 500**: `#9E9E9E` - 보조 텍스트 +- **Gray 600**: `#757575` - 아이콘 +- **Gray 700**: `#616161` - 부제목 +- **Gray 800**: `#424242` - 본문 텍스트 +- **Gray 900**: `#212121` - 제목 텍스트 + +#### White & Black +- **White**: `#FFFFFF` - 카드, 모달 배경 +- **Black**: `#000000` - 텍스트 (투명도 87%) + +### 3.5 색상 접근성 가이드 + +#### 텍스트 대비율 (WCAG 2.1 Level AA) +- **일반 텍스트**: 최소 4.5:1 +- **대형 텍스트** (18px 이상 또는 14px Bold): 최소 3:1 +- **UI 컴포넌트**: 최소 3:1 + +#### 색상 조합 예시 (통과) +- Primary 500 (`#2196F3`) on White - **대비율 3.1:1** ✅ (Large Text) +- Gray 900 (`#212121`) on White - **대비율 16.1:1** ✅ +- Gray 700 (`#616161`) on White - **대비율 5.7:1** ✅ +- White on Primary 700 (`#1976D2`) - **대비율 5.3:1** ✅ + +#### 색상만으로 정보 전달 금지 +- ❌ 빨간색 텍스트만으로 오류 표시 +- ✅ 빨간색 + 아이콘 + "오류" 텍스트 + +--- + +## 4. 타이포그래피 + +### 4.1 폰트 패밀리 + +#### Primary Font (한글) ```css ---primary: #0066CC; /* 주요 액션 버튼, 링크 */ ---primary-dark: #004A99; /* Hover, Active 상태 */ ---primary-light: #E6F2FF; /* 배경, 하이라이트 */ +font-family: 'Pretendard', -apple-system, BlinkMacSystemFont, system-ui, 'Noto Sans KR', sans-serif; ``` -### 2.2 Text Colors +**Pretendard 특징:** +- 한글 가독성 우수 +- 다양한 굵기 지원 (Thin~Black) +- 웹폰트 최적화 (가변 폰트 지원) +- 무료 라이선스 (OFL) + +**대체 폰트:** +- macOS/iOS: -apple-system (San Francisco) +- Windows: 맑은 고딕 (시스템 기본) +- 기타: Noto Sans KR + +#### Secondary Font (영문, 숫자) ```css ---text-primary: #1A1A1A; /* 제목, 본문 (15:1 대비) */ ---text-secondary: #666666; /* 부가 정보 (5:1 대비) */ ---text-disabled: #999999; /* 비활성 텍스트 */ ---text-inverse: #FFFFFF; /* 어두운 배경의 텍스트 */ +font-family: 'Inter', 'Roboto', Arial, sans-serif; ``` -### 2.3 Background Colors +**Inter 특징:** +- 숫자 가독성 우수 (Tabular Numbers) +- UI 최적화 +- 웹폰트 최적화 + +### 4.2 타입 스케일 + +#### Heading (제목) + +| 용도 | 크기 | 굵기 | 행간 | CSS Class | 사용 예시 | +|------|------|------|------|-----------|----------| +| **H1** | 32px | Bold (700) | 1.2 (38px) | `.text-h1` | 페이지 제목 | +| **H2** | 24px | Bold (700) | 1.3 (31px) | `.text-h2` | 섹션 제목 | +| **H3** | 20px | SemiBold (600) | 1.4 (28px) | `.text-h3` | 카드 제목 | +| **H4** | 18px | SemiBold (600) | 1.4 (25px) | `.text-h4` | 서브섹션 제목 | +| **H5** | 16px | Medium (500) | 1.5 (24px) | `.text-h5` | 그룹 제목 | +| **H6** | 14px | Medium (500) | 1.5 (21px) | `.text-h6` | 라벨 제목 | + +#### Body (본문) + +| 용도 | 크기 | 굵기 | 행간 | CSS Class | 사용 예시 | +|------|------|------|------|-----------|----------| +| **Body Large** | 16px | Regular (400) | 1.5 (24px) | `.text-body-lg` | 주요 본문 | +| **Body** | 14px | Regular (400) | 1.5 (21px) | `.text-body` | 기본 본문 | +| **Body Small** | 13px | Regular (400) | 1.5 (19px) | `.text-body-sm` | 보조 텍스트 | +| **Caption** | 12px | Regular (400) | 1.4 (17px) | `.text-caption` | 캡션, 메타 정보 | + +#### Special (특수) + +| 용도 | 크기 | 굵기 | 행간 | CSS Class | 사용 예시 | +|------|------|------|------|-----------|----------| +| **Button Large** | 16px | SemiBold (600) | 1.0 | `.text-btn-lg` | 주요 버튼 | +| **Button** | 14px | Medium (500) | 1.0 | `.text-btn` | 기본 버튼 | +| **Button Small** | 13px | Medium (500) | 1.0 | `.text-btn-sm` | 작은 버튼 | +| **Label** | 12px | Medium (500) | 1.0 | `.text-label` | 입력 레이블 | +| **Code** | 14px | Regular (400) | 1.5 | `.text-code` | 코드, 기술 정보 | + +### 4.3 타이포그래피 원칙 + +#### 가독성 우선 +- **최소 본문 크기**: 14px (모바일), 16px (데스크톱 권장) +- **적절한 행간**: 본문은 1.5 이상 +- **적절한 줄 길이**: 50-75자 (한글 기준 25-40자) + +#### 계층 구조 +- **제목과 본문의 명확한 구분**: 크기, 굵기, 색상 차이 +- **일관된 스케일**: 1.25배 비율 (Modular Scale) + +#### 색상 사용 +- **제목**: Gray 900 (`#212121`) +- **본문**: Gray 800 (`#424242`) +- **보조 텍스트**: Gray 600 (`#757575`) +- **비활성 텍스트**: Gray 400 (`#BDBDBD`) + +### 4.4 반응형 타이포그래피 + +#### Mobile (320px~767px) ```css ---bg-white: #FFFFFF; /* 주요 배경 */ ---bg-gray: #F5F5F5; /* 보조 배경 */ ---bg-dark: #1A1A1A; /* 다크 모드 배경 */ +/* H1 축소 */ +.text-h1 { font-size: 28px; } +.text-h2 { font-size: 22px; } + +/* Body 기본 유지 */ +.text-body { font-size: 14px; } ``` -### 2.4 Status Colors +#### Tablet (768px~1023px) ```css ---success: #0A7029; /* 성공, 완료 (4.5:1 대비) */ ---error: #C41E3A; /* 오류, 위험 (4.5:1 대비) */ ---warning: #856404; /* 경고, 주의 (4.5:1 대비) */ ---info: #0066CC; /* 정보 */ +/* 기본 스케일 유지 */ +.text-h1 { font-size: 32px; } +.text-body { font-size: 14px; } ``` -### 2.5 Border & Shadow +#### Desktop (1024px+) ```css ---border-light: #E0E0E0; /* 기본 테두리 */ ---border-medium: #CCCCCC; /* 강조 테두리 */ ---shadow-sm: 0 1px 3px rgba(0,0,0,0.1); ---shadow-md: 0 4px 6px rgba(0,0,0,0.1); ---shadow-lg: 0 10px 15px rgba(0,0,0,0.1); +/* Body 확대 */ +.text-body-lg { font-size: 18px; } +.text-body { font-size: 16px; } ``` --- -## 3. 타이포그래피 +## 5. 간격 시스템 -### 3.1 폰트 패밀리 +### 5.1 기본 단위 +**Base Unit: 4px** + +모든 간격은 4px의 배수를 사용하여 일관성 유지 + +### 5.2 간격 스케일 + +| Token | 값 | Rem | 사용 예시 | +|-------|-----|-----|----------| +| `spacing-0` | 0px | 0 | 간격 없음 | +| `spacing-1` | 4px | 0.25rem | 아이콘-텍스트 간격 | +| `spacing-2` | 8px | 0.5rem | 버튼 내부 패딩 (세로) | +| `spacing-3` | 12px | 0.75rem | 작은 요소 간격 | +| `spacing-4` | 16px | 1rem | 기본 요소 간격 | +| `spacing-5` | 20px | 1.25rem | 중간 요소 간격 | +| `spacing-6` | 24px | 1.5rem | 카드 내부 패딩 | +| `spacing-8` | 32px | 2rem | 섹션 간격 | +| `spacing-10` | 40px | 2.5rem | 큰 섹션 간격 | +| `spacing-12` | 48px | 3rem | 페이지 상하 여백 | +| `spacing-16` | 64px | 4rem | 특별한 강조 간격 | + +### 5.3 컴포넌트별 간격 + +#### 버튼 ```css ---font-primary: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, - "Helvetica Neue", Arial, sans-serif; ---font-mono: "SF Mono", Monaco, "Cascadia Code", monospace; +/* Primary Button */ +padding: 12px 24px; /* spacing-3 spacing-6 */ +gap: 8px; /* 아이콘-텍스트 간격 */ + +/* Small Button */ +padding: 8px 16px; /* spacing-2 spacing-4 */ ``` -### 3.2 폰트 크기 +#### 카드 ```css ---font-xs: 0.75rem; /* 12px - 캡션, 레이블 */ ---font-sm: 0.875rem; /* 14px - 본문, 입력 필드 */ ---font-base: 1rem; /* 16px - 기본 본문 */ ---font-lg: 1.125rem; /* 18px - 소제목 */ ---font-xl: 1.25rem; /* 20px - 제목 */ ---font-2xl: 1.5rem; /* 24px - 큰 제목 */ ---font-3xl: 2rem; /* 32px - 페이지 제목 */ +padding: 24px; /* spacing-6 */ +gap: 16px; /* 내부 요소 간격 */ ``` -### 3.3 폰트 굵기 +#### 폼 ```css ---font-regular: 400; ---font-medium: 500; ---font-semibold: 600; ---font-bold: 700; +/* Input Field */ +padding: 12px 16px; /* spacing-3 spacing-4 */ +margin-bottom: 16px; /* 필드 간격 */ + +/* Label */ +margin-bottom: 8px; /* spacing-2 */ ``` -### 3.4 행간 +#### 리스트 ```css ---leading-tight: 1.25; /* 제목 */ ---leading-normal: 1.5; /* 본문 */ ---leading-relaxed: 1.75; /* 긴 텍스트 */ +/* List Item */ +padding: 16px; /* spacing-4 */ +gap: 12px; /* 내부 요소 간격 */ + +/* List */ +gap: 8px; /* 항목 간격 */ ``` ---- +### 5.4 레이아웃 간격 -## 4. 간격 시스템 - -### 4.1 Spacing Scale +#### 페이지 여백 ```css ---space-1: 0.25rem; /* 4px */ ---space-2: 0.5rem; /* 8px */ ---space-3: 0.75rem; /* 12px */ ---space-4: 1rem; /* 16px */ ---space-5: 1.25rem; /* 20px */ ---space-6: 1.5rem; /* 24px */ ---space-8: 2rem; /* 32px */ ---space-10: 2.5rem; /* 40px */ ---space-12: 3rem; /* 48px */ ---space-16: 4rem; /* 64px */ +/* Mobile */ +padding: 16px; /* spacing-4 */ + +/* Tablet */ +padding: 24px; /* spacing-6 */ + +/* Desktop */ +padding: 32px; /* spacing-8 */ ``` -### 4.2 적용 규칙 -- **컴포넌트 내부 패딩**: space-4 (16px) -- **컴포넌트 간 간격**: space-6 (24px) -- **섹션 간 간격**: space-8 (32px) -- **페이지 여백**: space-4 (모바일), space-8 (데스크톱) - ---- - -## 5. 레이아웃 - -### 5.1 Breakpoints +#### 섹션 간격 ```css -/* Mobile First */ ---breakpoint-sm: 640px; /* 작은 태블릿 */ ---breakpoint-md: 768px; /* 태블릿 */ ---breakpoint-lg: 1024px; /* 데스크톱 */ ---breakpoint-xl: 1280px; /* 큰 데스크톱 */ -``` +/* 같은 그룹 내 섹션 */ +margin-bottom: 24px; /* spacing-6 */ -### 5.2 Container -```css ---container-mobile: 100%; ---container-tablet: 720px; ---container-desktop: 960px; ---container-wide: 1200px; +/* 다른 그룹 간 섹션 */ +margin-bottom: 48px; /* spacing-12 */ ``` -### 5.3 Grid System -- **Mobile (< 768px)**: 1 column -- **Tablet (768px - 1023px)**: 2 columns -- **Desktop (≥ 1024px)**: 3 columns -- **Gap**: 24px - --- ## 6. 컴포넌트 스타일 -### 6.1 버튼 +### 6.1 버튼 (Buttons) #### Primary Button +**주요 액션 (회의 시작, 저장, 공유 등)** + ```css -background: var(--primary); -color: var(--text-inverse); +background: Primary 500 (#2196F3); +color: White; padding: 12px 24px; border-radius: 8px; -font-weight: var(--font-semibold); -``` +font-weight: 600; +font-size: 16px; +box-shadow: 0 2px 4px rgba(0,0,0,0.1); -**상태:** -- Hover: `background: var(--primary-dark)` -- Active: `transform: scale(0.98)` -- Disabled: `opacity: 0.5; cursor: not-allowed` +/* Hover */ +background: Primary 600 (#1E88E5); +box-shadow: 0 4px 8px rgba(0,0,0,0.15); + +/* Active */ +background: Primary 700 (#1976D2); +transform: scale(0.98); + +/* Disabled */ +background: Gray 200 (#EEEEEE); +color: Gray 400 (#BDBDBD); +cursor: not-allowed; +``` #### Secondary Button -```css -background: transparent; -color: var(--primary); -border: 2px solid var(--primary); -padding: 10px 22px; -``` +**보조 액션 (취소, 건너뛰기 등)** -#### Ghost Button ```css -background: transparent; -color: var(--text-primary); +background: White; +color: Primary 500; +border: 1px solid Primary 500; padding: 12px 24px; +border-radius: 8px; + +/* Hover */ +background: Primary 50 (#E3F2FD); +border-color: Primary 600; ``` -### 6.2 입력 필드 +#### Text Button +**경량 액션 (더보기, 닫기 등)** ```css -border: 1px solid var(--border-light); +background: transparent; +color: Primary 500; +padding: 8px 16px; + +/* Hover */ +background: Primary 50; +``` + +#### Icon Button +**아이콘만 있는 버튼** + +```css +width: 44px; +height: 44px; /* 터치 영역 확보 */ +border-radius: 50%; +display: flex; +align-items: center; +justify-content: center; + +/* Hover */ +background: Gray 100; +``` + +#### Floating Action Button (FAB) +**화면 고정 액션 버튼** + +```css +position: fixed; +bottom: 24px; +right: 24px; +width: 56px; +height: 56px; +border-radius: 50%; +background: Primary 500; +box-shadow: 0 4px 12px rgba(33, 150, 243, 0.4); +z-index: 100; + +/* Hover */ +transform: scale(1.1); +box-shadow: 0 6px 16px rgba(33, 150, 243, 0.5); +``` + +### 6.2 입력 필드 (Input Fields) + +#### Text Input +```css +border: 1px solid Gray 300; border-radius: 8px; padding: 12px 16px; -font-size: var(--font-sm); +font-size: 16px; /* iOS 확대 방지 */ +background: White; transition: border-color 0.2s; + +/* Focus */ +border-color: Primary 500; +outline: 2px solid Primary 100; + +/* Error */ +border-color: Error (#F44336); +outline: 2px solid rgba(244, 67, 54, 0.1); + +/* Disabled */ +background: Gray 100; +color: Gray 400; +cursor: not-allowed; ``` -**상태:** -- Focus: `border-color: var(--primary); outline: 2px solid var(--primary-light)` -- Error: `border-color: var(--error)` -- Disabled: `background: var(--bg-gray); cursor: not-allowed` - -### 6.3 카드 - +#### Textarea ```css -background: var(--bg-white); +/* Text Input 속성 + */ +min-height: 100px; +resize: vertical; +``` + +#### Select / Dropdown +```css +/* Text Input 속성 + */ +appearance: none; +background-image: url('chevron-down-icon.svg'); +background-repeat: no-repeat; +background-position: right 12px center; +padding-right: 40px; +``` + +#### Checkbox +```css +width: 20px; +height: 20px; +border: 2px solid Gray 400; +border-radius: 4px; +cursor: pointer; + +/* Checked */ +background: Primary 500; +border-color: Primary 500; +/* Checkmark icon */ +``` + +#### Radio Button +```css +width: 20px; +height: 20px; +border: 2px solid Gray 400; +border-radius: 50%; + +/* Selected */ +border-color: Primary 500; +/* Inner dot */ +background: radial-gradient(Primary 500 40%, transparent 40%); +``` + +#### Toggle Switch +```css +width: 48px; +height: 24px; border-radius: 12px; -padding: var(--space-6); -box-shadow: var(--shadow-sm); -transition: box-shadow 0.3s; +background: Gray 300; +position: relative; + +/* Circle */ +width: 20px; +height: 20px; +border-radius: 50%; +background: White; +position: absolute; +left: 2px; +transition: left 0.2s; + +/* On State */ +background: Primary 500; +/* Circle moves right */ +left: 26px; ``` -**Hover:** +### 6.3 카드 (Cards) + +#### Standard Card ```css -box-shadow: var(--shadow-md); -transform: translateY(-2px); +background: White; +border-radius: 12px; +padding: 24px; +box-shadow: 0 1px 3px rgba(0,0,0,0.1); +transition: box-shadow 0.2s; + +/* Hover (클릭 가능한 경우) */ +box-shadow: 0 4px 12px rgba(0,0,0,0.15); +cursor: pointer; ``` -### 6.4 Badge/Chip - +#### Todo Card ```css +/* Standard Card + */ +border-left: 4px solid transparent; + +/* 완료 */ +border-left-color: Success (#4CAF50); +opacity: 0.7; + +/* 마감 임박 */ +border-left-color: Warning (#FF9800); + +/* 지연 */ +border-left-color: Error (#F44336); +``` + +#### Meeting Card +```css +/* Standard Card + */ +gap: 12px; +display: flex; +flex-direction: column; +``` + +### 6.4 배지 (Badges) + +#### Status Badge +```css +padding: 4px 12px; +border-radius: 12px; +font-size: 12px; +font-weight: 500; display: inline-flex; align-items: center; -padding: 4px 12px; +gap: 4px; + +/* 작성중 */ +background: Warning 50; +color: Warning 700; + +/* 확정완료 */ +background: Success 50; +color: Success 700; + +/* 검증완료 */ +background: Primary 50; +color: Primary 700; +``` + +#### Count Badge +```css +min-width: 20px; +height: 20px; +padding: 0 6px; +background: Error (#F44336); +color: White; +border-radius: 10px; +font-size: 11px; +font-weight: 600; +display: flex; +align-items: center; +justify-content: center; +``` + +### 6.5 모달 (Modals) + +#### Modal Overlay +```css +position: fixed; +top: 0; +left: 0; +right: 0; +bottom: 0; +background: rgba(0, 0, 0, 0.5); +z-index: 1000; +display: flex; +align-items: center; +justify-content: center; +``` + +#### Modal Container +```css +background: White; border-radius: 16px; -font-size: var(--font-xs); -font-weight: var(--font-medium); +max-width: 500px; +width: calc(100% - 32px); +max-height: 90vh; +overflow-y: auto; +box-shadow: 0 20px 60px rgba(0,0,0,0.3); + +/* Mobile: Full Screen */ +@media (max-width: 767px) { + width: 100%; + height: 100%; + max-height: 100vh; + border-radius: 0; +} ``` -**변형:** -- Success: `background: var(--success); color: white` -- Error: `background: var(--error); color: white` -- Info: `background: var(--primary-light); color: var(--primary)` +### 6.6 알림 (Toast / Snackbar) + +```css +position: fixed; +bottom: 24px; +left: 50%; +transform: translateX(-50%); +background: Gray 800; +color: White; +padding: 12px 24px; +border-radius: 8px; +box-shadow: 0 4px 12px rgba(0,0,0,0.3); +z-index: 2000; +animation: slideUp 0.3s ease-out; + +/* Success */ +background: Success (#4CAF50); + +/* Error */ +background: Error (#F44336); + +/* Warning */ +background: Warning (#FF9800); +``` --- -## 7. 아이콘 +## 7. 반응형 브레이크포인트 + +### 7.1 브레이크포인트 정의 -### 7.1 아이콘 크기 ```css ---icon-sm: 16px; /* 인라인 아이콘 */ ---icon-md: 24px; /* 버튼, 입력 필드 */ ---icon-lg: 32px; /* 큰 아이콘 */ ---icon-xl: 48px; /* 일러스트 아이콘 */ +/* Mobile Small */ +@media (min-width: 320px) { /* ... */ } + +/* Mobile */ +@media (min-width: 375px) { /* ... */ } + +/* Mobile Large */ +@media (min-width: 425px) { /* ... */ } + +/* Tablet */ +@media (min-width: 768px) { /* ... */ } + +/* Desktop */ +@media (min-width: 1024px) { /* ... */ } + +/* Desktop Large */ +@media (min-width: 1440px) { /* ... */ } ``` -### 7.2 아이콘 사용 원칙 -- SVG 형식 사용 -- `currentColor` 사용하여 텍스트 색상 상속 -- 접근성을 위해 `aria-label` 또는 `sr-only` 텍스트 제공 +### 7.2 디바이스별 최적화 + +#### Mobile (320px~767px) +- **레이아웃**: 단일 컬럼 +- **네비게이션**: 하단 탭 바 +- **터치 영역**: 최소 44x44px +- **폰트**: 기본 스케일 +- **간격**: 16px 페이지 여백 + +#### Tablet (768px~1023px) +- **레이아웃**: 2컬럼 (일부 화면) +- **네비게이션**: 좌측 사이드바 (선택) +- **폰트**: 기본 스케일 +- **간격**: 24px 페이지 여백 + +#### Desktop (1024px+) +- **레이아웃**: 2-3컬럼 +- **네비게이션**: 좌측 고정 사이드바 +- **폰트**: 확대 스케일 +- **간격**: 32px 페이지 여백 +- **추가 기능**: 단축키, 고급 필터 --- -## 8. 애니메이션 +## 8. 서비스 특화 컴포넌트 + +### 8.1 실시간 발언 표시기 + +**회의 진행 중 현재 발언자와 텍스트 표시** -### 8.1 Transition Duration ```css ---duration-fast: 150ms; /* Hover 효과 */ ---duration-base: 200ms; /* 일반 전환 */ ---duration-slow: 300ms; /* Modal, Drawer */ +.live-speech { + background: Accent 50 (#F3E5F5); + border-left: 4px solid Accent 500 (#9C27B0); + padding: 16px; + border-radius: 8px; + position: sticky; + top: 0; + z-index: 10; +} + +.speaker-name { + font-weight: 600; + color: Accent 700; + display: flex; + align-items: center; + gap: 8px; +} + +.speaking-indicator { + width: 8px; + height: 8px; + background: Error (#F44336); + border-radius: 50%; + animation: pulse 1.5s infinite; +} + +@keyframes pulse { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.5; } +} + +.speech-text { + color: Gray 800; + font-size: 14px; + line-height: 1.5; + margin-top: 8px; +} ``` -### 8.2 Easing Functions +### 8.2 전문용어 하이라이트 + +**AI가 감지한 전문용어 표시** + ```css ---ease-in: cubic-bezier(0.4, 0, 1, 1); ---ease-out: cubic-bezier(0, 0, 0.2, 1); ---ease-in-out: cubic-bezier(0.4, 0, 0.2, 1); +.term-highlight { + background: linear-gradient(180deg, transparent 60%, Accent 200 60%); + cursor: pointer; + position: relative; + border-bottom: 1px dotted Accent 500; + transition: background 0.2s; +} + +.term-highlight:hover { + background: Accent 100; +} + +/* 툴팁 */ +.term-tooltip { + position: absolute; + bottom: 100%; + left: 50%; + transform: translateX(-50%); + background: Gray 900; + color: White; + padding: 12px 16px; + border-radius: 8px; + font-size: 13px; + width: 250px; + box-shadow: 0 4px 12px rgba(0,0,0,0.3); + z-index: 100; +} ``` -### 8.3 애니메이션 원칙 -- 성능을 위해 `transform`과 `opacity`만 애니메이션 -- `prefers-reduced-motion` 미디어 쿼리 지원 -- 불필요한 애니메이션 지양 +### 8.3 검증 완료 섹션 + +**섹션별 검증 상태 표시** + +```css +.section-verified { + border: 2px solid Success (#4CAF50); + border-radius: 8px; + padding: 24px; + position: relative; +} + +.verified-badge { + position: absolute; + top: -12px; + right: 16px; + background: Success; + color: White; + padding: 4px 12px; + border-radius: 12px; + font-size: 12px; + font-weight: 600; + display: flex; + align-items: center; + gap: 4px; +} + +/* 검증자 아바타 */ +.verifier-avatars { + display: flex; + margin-top: 12px; +} + +.verifier-avatar { + width: 28px; + height: 28px; + border-radius: 50%; + border: 2px solid White; + margin-left: -8px; +} + +.verifier-avatar:first-child { + margin-left: 0; +} +``` + +### 8.4 실시간 동기화 인디케이터 + +**다른 사용자의 수정 사항 표시** + +```css +.sync-indicator { + position: fixed; + top: 80px; + right: 16px; + display: flex; + align-items: center; + gap: 8px; + padding: 8px 16px; + background: White; + border-radius: 20px; + box-shadow: 0 2px 8px rgba(0,0,0,0.1); + z-index: 50; +} + +.sync-dot { + width: 8px; + height: 8px; + background: Success; + border-radius: 50%; +} + +/* 동기화 중 */ +.sync-dot.syncing { + animation: blink 1s infinite; +} + +@keyframes blink { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.3; } +} + +/* 다른 사용자 편집 중 */ +.editing-highlight { + background: rgba(255, 152, 0, 0.1); + border-left: 3px solid Warning (#FF9800); + animation: fadeHighlight 3s ease-out; +} + +@keyframes fadeHighlight { + 0% { opacity: 1; } + 100% { opacity: 0; } +} +``` + +### 8.5 AI 처리 인디케이터 + +**AI가 처리 중임을 표시** + +```css +.ai-processing { + display: flex; + align-items: center; + gap: 8px; + padding: 12px 16px; + background: Accent 50; + border-radius: 8px; + border-left: 4px solid Accent 500; +} + +.ai-icon { + width: 20px; + height: 20px; + animation: rotate 2s linear infinite; +} + +@keyframes rotate { + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } +} + +.ai-text { + color: Accent 700; + font-size: 13px; + font-weight: 500; +} + +/* 신뢰도 표시 */ +.confidence-indicator { + display: flex; + gap: 4px; + margin-top: 4px; +} + +.confidence-bar { + height: 4px; + width: 40px; + background: Gray 200; + border-radius: 2px; + overflow: hidden; +} + +.confidence-fill { + height: 100%; + background: Success; + transition: width 0.3s; +} + +/* 높음: 80%+ = Success */ +/* 보통: 60-80% = Warning */ +/* 낮음: <60% = Error */ +``` + +### 8.6 Todo 진행 상태 카드 + +**Todo 완료 상태 시각화** + +```css +.todo-progress-card { + background: White; + border-radius: 12px; + padding: 20px; + box-shadow: 0 1px 3px rgba(0,0,0,0.1); +} + +/* 원형 진행 바 */ +.circular-progress { + width: 100px; + height: 100px; + border-radius: 50%; + background: conic-gradient( + Primary 500 var(--progress-percent), + Gray 200 var(--progress-percent) + ); + display: flex; + align-items: center; + justify-content: center; + position: relative; +} + +.progress-inner { + width: 80px; + height: 80px; + background: White; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + flex-direction: column; +} + +.progress-percent { + font-size: 24px; + font-weight: 700; + color: Primary 500; +} + +.progress-label { + font-size: 12px; + color: Gray 600; +} +``` --- -## 9. 접근성 +## 9. 인터랙션 패턴 -### 9.1 색상 대비 -- 일반 텍스트: 최소 4.5:1 -- 큰 텍스트 (18pt+ 또는 14pt bold+): 최소 3:1 -- UI 컴포넌트: 최소 3:1 +### 9.1 터치 인터랙션 (Mobile) -### 9.2 포커스 표시 +#### 기본 터치 영역 ```css -*:focus-visible { - outline: 2px solid var(--primary); +/* 최소 터치 영역: 44x44px */ +min-width: 44px; +min-height: 44px; +``` + +#### 스와이프 액션 +**리스트 항목 스와이프로 빠른 액션** + +- **좌→우 스와이프**: 완료 처리 (Todo) +- **우→좌 스와이프**: 삭제/편집 메뉴 표시 + +```css +.swipe-action { + position: absolute; + right: 0; + top: 0; + bottom: 0; + display: flex; + transform: translateX(100%); + transition: transform 0.3s; +} + +.swiped .swipe-action { + transform: translateX(0); +} + +.swipe-button { + width: 80px; + display: flex; + align-items: center; + justify-content: center; +} + +.swipe-button.delete { + background: Error (#F44336); +} + +.swipe-button.edit { + background: Primary 500; +} +``` + +#### Pull to Refresh +**화면 당겨서 새로고침** + +```css +.pull-refresh-indicator { + position: absolute; + top: -60px; + left: 50%; + transform: translateX(-50%); + display: flex; + align-items: center; + gap: 8px; + transition: top 0.3s; +} + +.pulling .pull-refresh-indicator { + top: 20px; +} + +.refresh-icon { + animation: rotate 1s linear infinite; +} +``` + +### 9.2 키보드 인터랙션 (Desktop) + +#### 포커스 표시 +```css +*:focus { + outline: 2px solid Primary 500; outline-offset: 2px; } + +/* 마우스 클릭 시 포커스 숨김 */ +*:focus:not(:focus-visible) { + outline: none; +} ``` -### 9.3 터치 타겟 -- 최소 크기: 44x44px -- 간격: 최소 8px - -### 9.4 스크린 리더 +#### 단축키 안내 ```css -.sr-only { - position: absolute; - width: 1px; - height: 1px; - padding: 0; - margin: -1px; +.keyboard-hint { + position: fixed; + bottom: 16px; + right: 16px; + background: Gray 800; + color: White; + padding: 8px 12px; + border-radius: 6px; + font-size: 12px; + opacity: 0.8; +} + +.kbd { + background: Gray 700; + padding: 2px 6px; + border-radius: 4px; + font-family: monospace; + font-size: 11px; + border: 1px solid Gray 600; +} +``` + +**주요 단축키:** +- `Ctrl/Cmd + S`: 저장 +- `Ctrl/Cmd + K`: 검색 +- `Esc`: 모달 닫기 +- `Tab`: 다음 요소 +- `Shift + Tab`: 이전 요소 +- `Enter`: 확인/실행 + +### 9.3 로딩 상태 + +#### Spinner +```css +.spinner { + width: 40px; + height: 40px; + border: 4px solid Gray 200; + border-top-color: Primary 500; + border-radius: 50%; + animation: spin 1s linear infinite; +} + +@keyframes spin { + to { transform: rotate(360deg); } +} +``` + +#### Skeleton UI +```css +.skeleton { + background: linear-gradient( + 90deg, + Gray 200 0%, + Gray 100 50%, + Gray 200 100% + ); + background-size: 200% 100%; + animation: loading 1.5s infinite; + border-radius: 4px; +} + +@keyframes loading { + 0% { background-position: 200% 0; } + 100% { background-position: -200% 0; } +} + +.skeleton-text { + height: 16px; + margin-bottom: 8px; +} + +.skeleton-title { + height: 24px; + width: 60%; + margin-bottom: 12px; +} + +.skeleton-avatar { + width: 40px; + height: 40px; + border-radius: 50%; +} +``` + +#### Progress Bar +```css +.progress-bar { + width: 100%; + height: 4px; + background: Gray 200; + border-radius: 2px; overflow: hidden; - clip: rect(0, 0, 0, 0); +} + +.progress-fill { + height: 100%; + background: Primary 500; + transition: width 0.3s; +} + +/* Indeterminate (불명확한 진행) */ +.progress-fill.indeterminate { + width: 30%; + animation: indeterminate 1.5s infinite; +} + +@keyframes indeterminate { + 0% { transform: translateX(-100%); } + 100% { transform: translateX(400%); } +} +``` + +### 9.4 실시간 협업 피드백 + +#### 사용자 커서 표시 +```css +.user-cursor { + position: absolute; + pointer-events: none; + z-index: 999; +} + +.cursor-flag { + background: var(--user-color); + color: White; + padding: 2px 6px; + border-radius: 4px; + font-size: 11px; white-space: nowrap; - border-width: 0; + transform: translate(-50%, -100%); + margin-top: -4px; +} + +.cursor-pointer { + width: 0; + height: 0; + border-left: 6px solid transparent; + border-right: 6px solid transparent; + border-top: 8px solid var(--user-color); +} +``` + +#### 편집 중 표시 +```css +.editing-user { + background: rgba(var(--user-color-rgb), 0.1); + border-left: 3px solid var(--user-color); + padding-left: 12px; +} + +.editing-badge { + display: inline-flex; + align-items: center; + gap: 4px; + background: var(--user-color); + color: White; + padding: 2px 8px; + border-radius: 10px; + font-size: 11px; + font-weight: 500; } ``` --- -## 10. 다크 모드 +## 10. 아이콘 시스템 + +### 10.1 아이콘 스타일 + +**아이콘 세트**: Material Icons (Outlined) + +**스타일 가이드:** +- **스타일**: Outlined (선 스타일) +- **일관성**: 동일한 두께(2px), 동일한 라운딩 +- **크기**: 20px (Small), 24px (Default), 32px (Large) + +### 10.2 주요 아이콘 매핑 + +| 기능 | 아이콘 이름 | 사용 위치 | +|------|------------|----------| +| 회의 시작 | `play_circle` | 회의 시작 버튼 | +| 회의 종료 | `stop_circle` | 회의 종료 버튼 | +| 녹음 | `mic` | 녹음 상태 표시 | +| 일시정지 | `pause_circle` | 녹음 일시정지 | +| 저장 | `save` | 저장 버튼 | +| 공유 | `share` | 공유 버튼 | +| 편집 | `edit` | 수정 버튼 | +| 삭제 | `delete` | 삭제 버튼 | +| 검색 | `search` | 검색 입력 | +| 필터 | `filter_list` | 필터 메뉴 | +| 설정 | `settings` | 설정 메뉴 | +| 알림 | `notifications` | 알림 아이콘 | +| 프로필 | `account_circle` | 사용자 프로필 | +| 캘린더 | `calendar_today` | 날짜 선택 | +| 시간 | `schedule` | 시간 선택 | +| 첨부파일 | `attach_file` | 파일 첨부 | +| 다운로드 | `download` | 파일 다운로드 | +| 체크 | `check_circle` | 완료, 검증 | +| 경고 | `warning` | 경고 메시지 | +| 오류 | `error` | 오류 메시지 | +| 정보 | `info` | 정보 메시지 | +| AI | `auto_awesome` | AI 기능 | +| Todo | `check_box` | Todo 항목 | +| 참석자 | `group` | 참석자 목록 | +| 더보기 | `more_vert` | 메뉴 | +| 뒤로가기 | `arrow_back` | 이전 화면 | +| 닫기 | `close` | 모달 닫기 | +| 펼치기 | `expand_more` | 아코디언 열기 | +| 접기 | `expand_less` | 아코디언 닫기 | + +### 10.3 아이콘 색상 ```css -@media (prefers-color-scheme: dark) { - :root { - --bg-white: #1A1A1A; - --bg-gray: #2A2A2A; - --text-primary: #FFFFFF; - --text-secondary: #CCCCCC; - --border-light: #404040; - --shadow-sm: 0 1px 3px rgba(0,0,0,0.3); +/* Default */ +color: Gray 600 (#757575); + +/* Active/Selected */ +color: Primary 500 (#2196F3); + +/* Disabled */ +color: Gray 400 (#BDBDBD); + +/* On Color Background */ +color: White; + +/* Semantic */ +.icon-success { color: Success (#4CAF50); } +.icon-warning { color: Warning (#FF9800); } +.icon-error { color: Error (#F44336); } +.icon-ai { color: Accent 500 (#9C27B0); } +``` + +--- + +## 11. 애니메이션 가이드 + +### 11.1 애니메이션 원칙 + +- **목적성**: 모든 애니메이션은 명확한 목적이 있어야 함 +- **자연스러움**: 현실 세계의 물리 법칙을 따름 +- **적절한 속도**: 너무 빠르거나 느리지 않게 +- **성능**: CSS 애니메이션 우선, GPU 가속 활용 + +### 11.2 이징 (Easing) + +```css +/* 표준 Easing */ +--ease-in-out: cubic-bezier(0.4, 0, 0.2, 1); /* Material Design */ +--ease-out: cubic-bezier(0.0, 0, 0.2, 1); +--ease-in: cubic-bezier(0.4, 0, 1, 1); + +/* 탄성 (Elastic) */ +--ease-elastic: cubic-bezier(0.68, -0.55, 0.265, 1.55); +``` + +### 11.3 지속 시간 (Duration) + +| 용도 | 지속 시간 | 예시 | +|------|----------|------| +| **Instant** | 0ms | 즉시 변경 | +| **Quick** | 100ms | 호버 효과, 색상 변경 | +| **Normal** | 200-300ms | 모달 열기, 탭 전환 | +| **Slow** | 400-500ms | 페이지 전환, 복잡한 애니메이션 | + +### 11.4 애니메이션 라이브러리 + +#### 페이드 인 +```css +@keyframes fadeIn { + from { + opacity: 0; + } + to { + opacity: 1; } } + +.fade-in { + animation: fadeIn 0.3s var(--ease-out); +} +``` + +#### 슬라이드 업 +```css +@keyframes slideUp { + from { + transform: translateY(20px); + opacity: 0; + } + to { + transform: translateY(0); + opacity: 1; + } +} + +.slide-up { + animation: slideUp 0.3s var(--ease-out); +} +``` + +#### 스케일 +```css +@keyframes scaleIn { + from { + transform: scale(0.9); + opacity: 0; + } + to { + transform: scale(1); + opacity: 1; + } +} + +.scale-in { + animation: scaleIn 0.2s var(--ease-out); +} +``` + +#### 쉐이크 (오류 시) +```css +@keyframes shake { + 0%, 100% { transform: translateX(0); } + 10%, 30%, 50%, 70%, 90% { transform: translateX(-5px); } + 20%, 40%, 60%, 80% { transform: translateX(5px); } +} + +.shake { + animation: shake 0.5s var(--ease-in-out); +} +``` + +### 11.5 성능 최적화 + +**GPU 가속 속성만 사용:** +- ✅ `transform` (translate, scale, rotate) +- ✅ `opacity` +- ❌ `left`, `top` (리플로우 발생) +- ❌ `width`, `height` (리플로우 발생) + +**will-change 활용:** +```css +.will-animate { + will-change: transform, opacity; +} + +/* 애니메이션 완료 후 제거 */ +.animated { + will-change: auto; +} ``` --- -## 11. 모바일 최적화 +## 12. 상태 표시 -### 11.1 터치 최적화 -- 버튼/링크 최소 크기: 44x44px -- 스와이프 제스처 지원 -- Pull-to-refresh 지원 +### 12.1 회의록 상태 -### 11.2 성능 최적화 -- 이미지 lazy loading -- 코드 스플리팅 -- CSS 최소화 -- 시스템 폰트 우선 사용 +| 상태 | 색상 | 배지 텍스트 | 설명 | +|------|------|-----------|------| +| **작성중** | Warning | `작성중` | 회의 진행 중 또는 수정 중 | +| **확정완료** | Success | `확정완료` | 최종 확정된 회의록 | +| **공유됨** | Primary | `공유됨` | 참석자에게 공유됨 | + +### 12.2 Todo 상태 + +| 상태 | 색상 | 표시 | 설명 | +|------|------|------|------| +| **진행중** | Gray | `○` | 할당되었으나 미완료 | +| **완료** | Success | `✓` | 완료 처리됨 | +| **마감 임박** | Warning | `⚠` | 3일 이내 마감 | +| **지연** | Error | `!` | 마감일 경과 | + +### 12.3 검증 상태 + +| 상태 | 표시 | 설명 | +|------|------|------| +| **미검증** | 빈 원 | 아직 검증되지 않음 | +| **검증 중** | 부분 체크 | 일부 참석자 검증 | +| **검증 완료** | 체크 아이콘 + 배지 | 모든 참석자 검증 | +| **잠김** | 자물쇠 아이콘 | 수정 불가 | + +### 12.4 동기화 상태 + +| 상태 | 표시 | 설명 | +|------|------|------| +| **동기화됨** | 녹색 점 | 최신 상태 | +| **동기화 중** | 깜빡이는 점 | 서버와 동기화 중 | +| **오프라인** | 회색 점 | 네트워크 끊김 (로컬 저장) | +| **충돌** | 빨간색 느낌표 | 동시 수정 충돌 | + +### 12.5 AI 처리 상태 + +| 상태 | 표시 | 설명 | +|------|------|------| +| **처리 중** | 회전 아이콘 | AI 분석 중 | +| **완료** | 체크 아이콘 | 처리 완료 | +| **오류** | 오류 아이콘 | 처리 실패 | +| **신뢰도 높음** | 3개 바 녹색 | 80% 이상 | +| **신뢰도 보통** | 2개 바 노란색 | 60-80% | +| **신뢰도 낮음** | 1개 바 빨간색 | 60% 미만 | --- -## 12. 컴포넌트 라이브러리 +## 13. 접근성 표준 -### 12.1 공통 컴포넌트 -1. **Header**: 페이지 상단 네비게이션 -2. **Button**: Primary, Secondary, Ghost -3. **Input**: Text, Email, Password, Date, Time -4. **Card**: 정보 카드, 클릭 가능한 카드 -5. **Badge**: 상태 표시 -6. **Modal**: 팝업 다이얼로그 -7. **Toast**: 알림 메시지 -8. **Loading**: 로딩 스피너 -9. **Empty State**: 빈 상태 일러스트 -10. **Bottom Navigation**: 모바일 하단 네비게이션 +### 13.1 WCAG 2.1 Level AA 준수 -### 12.2 도메인 컴포넌트 -1. **Meeting Card**: 회의 정보 카드 -2. **Minutes Editor**: 회의록 편집기 -3. **Todo Item**: Todo 항목 -4. **Attendee Avatar**: 참석자 아바타 -5. **Term Tooltip**: 전문용어 툴팁 +#### Perceivable (인식 가능) +- ✅ 모든 이미지에 대체 텍스트 제공 +- ✅ 색상 대비 4.5:1 이상 (일반 텍스트) +- ✅ 색상 대비 3:1 이상 (대형 텍스트, UI 컴포넌트) +- ✅ 텍스트 200% 확대 시 레이아웃 유지 +- ✅ 색상만으로 정보 전달 금지 ---- +#### Operable (조작 가능) +- ✅ 모든 기능 키보드로 접근 가능 +- ✅ 포커스 순서 논리적 +- ✅ 포커스 표시 명확 (2px 파란색 테두리) +- ✅ 터치 영역 최소 44x44px +- ✅ 시간 제한 없음 (자동 저장) -## 13. 사용 예시 +#### Understandable (이해 가능) +- ✅ 명확한 레이블 및 안내 +- ✅ 오류 메시지 구체적 +- ✅ 일관된 네비게이션 +- ✅ 예측 가능한 동작 -### 13.1 페이지 구조 +#### Robust (견고함) +- ✅ 시맨틱 HTML 사용 +- ✅ ARIA 속성 적절히 사용 +- ✅ 최신 브라우저 및 보조 기술 지원 + +### 13.2 ARIA 속성 가이드 + +#### 랜드마크 ```html - - - - - - 페이지 제목 - - - -
- -
- -
- -
- - - - +
+ +
+ +
``` -### 13.2 버튼 사용 +#### 버튼 및 링크 ```html - - + + - - - - - - - - + + + 링크 텍스트 + + ``` -### 13.3 입력 필드 +#### 폼 ```html -
- - - 회의 제목을 입력해주세요 + + + + + +``` + +#### 모달 +```html +
+ +
``` +#### 실시간 업데이트 +```html + +
+ 회의록이 저장되었습니다. +
+ + +
+ 3명이 편집 중입니다. +
+ + +
+ +
+``` + +### 13.3 스크린 리더 테스트 + +**주요 테스트 도구:** +- **NVDA** (Windows, 무료) +- **JAWS** (Windows, 유료) +- **VoiceOver** (macOS/iOS, 내장) +- **TalkBack** (Android, 내장) + +**테스트 체크리스트:** +- [ ] 모든 이미지에 적절한 alt 텍스트 +- [ ] 모든 폼 필드에 레이블 연결 +- [ ] 버튼 및 링크 용도 명확 +- [ ] 모달 열림/닫힘 알림 +- [ ] 오류 메시지 즉시 알림 +- [ ] 실시간 업데이트 알림 +- [ ] 키보드만으로 전체 기능 사용 가능 + --- ## 14. 변경 이력 -| 버전 | 날짜 | 변경 내용 | -|------|------|-----------| -| 1.0 | 2025-01-20 | 초기 스타일 가이드 작성 | +| 버전 | 날짜 | 작성자 | 변경 내용 | +|------|------|--------|----------| +| 1.0 | 2025-10-21 | 이미준 | 최초 작성 - 14개 섹션 완료 | + +--- + +## 부록 + +### A. 개발 참고 자료 + +#### CSS 변수 정의 +```css +:root { + /* Primary Colors */ + --primary-50: #E3F2FD; + --primary-500: #2196F3; + --primary-700: #1976D2; + + /* Secondary Colors */ + --secondary-500: #4CAF50; + --accent-500: #9C27B0; + + /* Semantic Colors */ + --success: #4CAF50; + --warning: #FF9800; + --error: #F44336; + --info: #2196F3; + + /* Neutral Colors */ + --gray-50: #FAFAFA; + --gray-800: #424242; + --gray-900: #212121; + + /* Spacing */ + --spacing-2: 8px; + --spacing-4: 16px; + --spacing-6: 24px; + + /* Typography */ + --font-primary: 'Pretendard', sans-serif; + --font-secondary: 'Inter', sans-serif; + + /* Border Radius */ + --radius-sm: 4px; + --radius-md: 8px; + --radius-lg: 12px; + --radius-full: 9999px; + + /* Shadows */ + --shadow-sm: 0 1px 3px rgba(0,0,0,0.1); + --shadow-md: 0 4px 12px rgba(0,0,0,0.15); + --shadow-lg: 0 20px 60px rgba(0,0,0,0.3); + + /* Transitions */ + --transition-fast: 0.1s; + --transition-normal: 0.2s; + --transition-slow: 0.3s; +} +``` + +### B. 디자인 도구 + +**권장 도구:** +- **Figma**: UI 디자인 및 프로토타입 +- **Material Icons**: 아이콘 세트 +- **Coolors**: 색상 팔레트 생성 +- **WebAIM Contrast Checker**: 색상 대비 검사 + +### C. 품질 검사 도구 + +**접근성:** +- Lighthouse (Chrome DevTools) +- axe DevTools +- WAVE Browser Extension + +**성능:** +- WebPageTest +- Chrome DevTools Performance +- React DevTools Profiler + +### D. 참고 문헌 + +- Material Design 3 Guidelines +- Apple Human Interface Guidelines +- WCAG 2.1 Level AA +- Mobile First Design - Luke Wroblewski +- Inclusive Components - Heydon Pickering diff --git a/design-last/uiux_다람지/uiux.md b/design/uiux_다람지/uiux.md similarity index 100% rename from design-last/uiux_다람지/uiux.md rename to design/uiux_다람지/uiux.md diff --git a/design/userstory-table.md b/design/userstory-table.md deleted file mode 100644 index 6d64b52..0000000 --- a/design/userstory-table.md +++ /dev/null @@ -1,300 +0,0 @@ -# 회의록 작성 및 공유 개선 서비스 - 유저스토리 목록 - -## 문서 정보 -- **작성일**: 2025-01-20 -- **버전**: 1.0 -- **기반 문서**: design-last/userstory.md - ---- - -## 목차 -1. [차별화 전략](#차별화-전략) -2. [마이크로서비스 구성](#마이크로서비스-구성) -3. [전체 유저스토리 목록](#전체-유저스토리-목록) -4. [서비스별 유저스토리](#서비스별-유저스토리) - ---- - -## 차별화 전략 - -### 기본 기능 (Hygiene Factors) -- **STT(Speech To Text)**: 음성을 텍스트로 변환하는 기본 기능 - - 시장의 대부분 서비스가 제공 - - 차별화 포인트 아님 - -### 핵심 차별화 포인트 (Differentiators) -| 차별화 기능 | 설명 | -|------------|------| -| **맥락 기반 용어 설명** | 관련 회의록과 업무이력 기반 실용적 정보 제공 | -| **강화된 Todo 연결** | Action item과 담당자 Todo 실시간 연결 및 자동 반영 | -| **프롬프팅 기반 회의록 개선** | AI를 활용한 다양한 형식의 회의록 생성 | -| **지능형 회의 진행 지원** | 회의 패턴 분석을 통한 안건 추천 및 효율성 분석 | - ---- - -## 마이크로서비스 구성 - -| 순번 | 서비스명 | 책임 | 차별화 여부 | -|------|---------|------|------------| -| 1 | User | 사용자 인증 및 권한 관리 | 기본 | -| 2 | Meeting | 회의 관리, 회의록 생성/관리/공유 | 기본 | -| 3 | STT | 음성 녹음 관리, 음성-텍스트 변환, 화자 식별 | 기본 | -| 4 | AI | LLM 기반 회의록 자동 작성, Todo 자동 추출, 프롬프팅 기반 회의록 개선 | **차별화** | -| 5 | RAG | 맥락 기반 용어 설명, 관련 문서 검색 및 연결, 업무 이력 통합 | **차별화** | -| 6 | Collaboration | 실시간 동기화, 버전 관리, 충돌 해결 | 기본 | -| 7 | Todo | Todo 할당 및 관리, 진행 상황 추적, 회의록 실시간 연동 | **차별화** | -| 8 | Notification | 알림 발송 및 리마인더 관리 | 기본 | - ---- - -## 전체 유저스토리 목록 - -| 번호 | ID | 서비스 | 기능명 | As a | I want | So that | 복잡도 | 차별화 | -|------|----|---------|---------|-----------------------------|------------------------------------------|----------------------------------------|--------|--------| -| 1 | AFR-USER-010 | User | 사용자 인증 관리 | 시스템 관리자 | 사용자 인증 기능 | 서비스 보안 유지 | M/8 | ❌ | -| 2 | UFR-MEET-010 | Meeting | 회의 예약 | 회의록 작성자 | 회의를 예약하고 참석자를 초대 | 회의를 효율적으로 준비 | M/13 | ❌ | -| 3 | UFR-MEET-020 | Meeting | 템플릿 선택 | 회의록 작성자 | 회의 유형에 맞는 템플릿을 선택 | 회의록을 효율적으로 작성 | S/5 | ❌ | -| 4 | UFR-MEET-030 | Meeting | 회의 시작 | 회의록 작성자 | 회의를 시작하고 음성 녹음을 준비 | 회의록을 작성 | M/8 | ❌ | -| 5 | UFR-MEET-040 | Meeting | 회의 종료 | 회의록 작성자 | 회의를 종료하고 통계를 확인 | 회의를 정리 | M/8 | ❌ | -| 6 | UFR-MEET-050 | Meeting | 최종 확정 | 회의록 작성자 | 최종 회의록을 확정하고 버전을 생성 | 회의록을 완성 | M/13 | ❌ | -| 7 | UFR-MEET-060 | Meeting | 회의록 공유 | 회의록 작성자 | 최종 회의록을 공유 | 회의 내용을 참석자들과 공유 | M/13 | ❌ | -| 8 | UFR-STT-010 | STT | 음성 녹음 인식 | 회의 참석자 | 음성이 실시간으로 녹음되고 인식 | 발언 내용이 자동으로 기록 | M/21 | ❌ | -| 9 | UFR-STT-020 | STT | 텍스트 변환 | 회의록 시스템 | 음성을 텍스트로 변환 | 인식된 발언을 회의록에 기록 | M/13 | ❌ | -| 10 | UFR-AI-010 | AI | 회의록 자동 작성 | 회의록 작성자 | AI가 발언 내용을 자동으로 정리하여 회의록을 작성 | 회의록 작성 부담을 줄임 | M/34 | ❌ | -| 11 | UFR-AI-020 | AI | Todo 자동 추출 | 회의록 작성자 | AI가 회의록에서 Todo 항목을 자동으로 추출하고 담당자를 식별 | 회의 후 실행 사항을 명확히 함 | M/21 | ✅ | -| 12 | UFR-AI-030 | AI | 회의록 개선 | 회의록 작성자 | 프롬프팅을 통해 회의록을 개선하고 재구성 | 회의록을 다양한 형식으로 변환 | M/21 | ✅ | -| 13 | UFR-AI-040 | AI | 관련 회의록 연결 | 회의록 작성자 | AI가 관련 있는 과거 회의록을 자동으로 찾아 연결 | 이전 회의 내용을 쉽게 참조 | S/13 | ✅ | -| 14 | UFR-RAG-010 | RAG | 전문용어 감지 | 회의록 작성자 | 전문용어가 자동으로 감지되고 맥락에 맞는 설명을 제공 | 업무 지식이 없어도 회의록을 정확히 작성 | S/13 | ✅ | -| 15 | UFR-RAG-020 | RAG | 맥락 기반 용어 설명 | 회의록 작성자 | 관련 회의록과 업무 이력을 바탕으로 실용적인 설명을 제공 | 전문용어를 맥락에 맞게 이해 | S/21 | ✅ | -| 16 | UFR-COLLAB-010 | Collaboration | 회의록 수정 동기화 | 회의 참석자 | 회의록을 수정하고 실시간으로 다른 참석자와 동기화 | 회의록을 함께 검증 | M/34 | ❌ | -| 17 | UFR-COLLAB-020 | Collaboration | 충돌 해결 | 회의 참석자 | 충돌을 감지하고 해결 | 동시 수정 상황에서도 내용을 잃지 않음 | M/21 | ❌ | -| 18 | UFR-COLLAB-030 | Collaboration | 검증 완료 | 회의 참석자 | 주요 섹션을 검증하고 완료 표시 | 회의록의 정확성을 보장 | M/8 | ❌ | -| 19 | UFR-TODO-010 | Todo | Todo 할당 | Todo 시스템 | Todo를 실시간으로 할당하고 회의록과 연결 | AI가 추출한 Todo를 담당자에게 전달 | M/13 | ✅ | -| 20 | UFR-TODO-030 | Todo | Todo 완료 처리 | Todo 담당자 | Todo를 완료하고 회의록에 자동 반영 | 완료된 Todo를 처리하고 회의록에 반영 | M/8 | ✅ | - -**총 20개 유저스토리** (차별화 기능 7개 ✅) - ---- - -## 서비스별 유저스토리 - -### 1. User 서비스 (1개) - -| ID | 기능명 | As a | I want | So that | 복잡도 | -|----|--------|------|--------|---------|--------| -| AFR-USER-010 | 사용자 인증 관리 | 시스템 관리자 | 사용자 인증 기능을 원한다 | 서비스 보안을 위해 | M/8 | - -#### AFR-USER-010: 사용자 인증 관리 - -**시나리오**: 사용자 인증 관리 -- 사용자가 로그인을 시도한 상황에서 -- 사번과 비밀번호를 입력하면 -- LDAP 연동을 통해 인증이 완료되고 권한에 따라 서비스에 접근할 수 있다 - -**주요 기능**: -- 사용자 인증 (사번, 비밀번호) -- 세션 관리 - ---- - -### 2. Meeting 서비스 (6개) - -| ID | 기능명 | As a | I want | So that | 복잡도 | -|----|--------|------|--------|---------|--------| -| UFR-MEET-010 | 회의 예약 | 회의록 작성자 | 회의를 예약하고 참석자를 초대하고 싶다 | 회의를 효율적으로 준비하기 위해 | M/13 | -| UFR-MEET-020 | 템플릿 선택 | 회의록 작성자 | 회의 유형에 맞는 템플릿을 선택하고 싶다 | 회의록을 효율적으로 작성하기 위해 | S/5 | -| UFR-MEET-030 | 회의 시작 | 회의록 작성자 | 회의를 시작하고 음성 녹음을 준비하고 싶다 | 회의를 시작하고 회의록을 작성하기 위해 | M/8 | -| UFR-MEET-040 | 회의 종료 | 회의록 작성자 | 회의를 종료하고 통계를 확인하고 싶다 | 회의를 종료하고 회의록을 정리하기 위해 | M/8 | -| UFR-MEET-050 | 최종 확정 | 회의록 작성자 | 최종 회의록을 확정하고 버전을 생성하고 싶다 | 회의록을 완성하기 위해 | M/13 | -| UFR-MEET-060 | 회의록 공유 | 회의록 작성자 | 최종 회의록을 공유하고 싶다 | 회의 내용을 참석자들과 공유하기 위해 | M/13 | - -#### UFR-MEET-010: 회의 예약 - -**시나리오**: 회의 예약 및 참석자 초대 -- 회의 예약 화면에 접근한 상황에서 -- 회의 제목, 날짜/시간, 장소, 참석자 목록을 입력하고 예약 버튼을 클릭하면 -- 회의가 예약되고 참석자에게 초대 이메일이 자동 발송된다 - -**입력 요구사항**: -- 회의 제목: 최대 100자 (필수) -- 날짜/시간: 날짜 및 시간 선택 (필수) -- 장소: 최대 200자 (선택) -- 참석자 목록: 이메일 주소 입력 (최소 1명 필수) - -**처리 결과**: -- 회의가 예약됨 (회의 ID 생성) -- 일정이 캘린더에 자동 등록됨 -- 참석자에게 초대 이메일 발송됨 -- 회의 시작 30분 전 리마인더 자동 발송 - ---- - -### 3. STT 서비스 (2개) - 기본 기능 - -| ID | 기능명 | As a | I want | So that | 복잡도 | -|----|--------|------|--------|---------|--------| -| UFR-STT-010 | 음성 녹음 인식 | 회의 참석자 | 음성이 실시간으로 녹음되고 인식되기를 원한다 | 발언 내용이 자동으로 기록되기 위해 | M/21 | -| UFR-STT-020 | 텍스트 변환 | 회의록 시스템 | 음성을 텍스트로 변환하고 싶다 | 인식된 발언을 회의록에 기록하기 위해 | M/13 | - -**비고**: STT는 기본 기능으로 경쟁사 대부분이 제공하는 기능임 (차별화 포인트 아님) - ---- - -### 4. AI 서비스 (4개) - 차별화 포인트 ✅ - -| ID | 기능명 | As a | I want | So that | 복잡도 | 차별화 | -|----|--------|------|--------|---------|--------|--------| -| UFR-AI-010 | 회의록 자동 작성 | 회의록 작성자 | AI가 발언 내용을 자동으로 정리하여 회의록을 작성하기를 원한다 | 회의록 작성 부담을 줄이기 위해 | M/34 | ❌ | -| UFR-AI-020 | Todo 자동 추출 | 회의록 작성자 | AI가 회의록에서 Todo 항목을 자동으로 추출하고 담당자를 식별하기를 원한다 | 회의 후 실행 사항을 명확히 하기 위해 | M/21 | ✅ | -| UFR-AI-030 | 회의록 개선 | 회의록 작성자 | 프롬프팅을 통해 회의록을 개선하고 재구성하고 싶다 | 회의록을 다양한 형식으로 변환하기 위해 | M/21 | ✅ | -| UFR-AI-040 | 관련 회의록 연결 | 회의록 작성자 | AI가 관련 있는 과거 회의록을 자동으로 찾아 연결해주기를 원한다 | 이전 회의 내용을 쉽게 참조하기 위해 | S/13 | ✅ | - -#### UFR-AI-030: 회의록 개선 (차별화 포인트) - -**시나리오**: 프롬프팅 기반 회의록 개선 -- 회의록이 작성된 상황에서 -- "1Page 요약", "핵심 요약", "상세 보고서" 등의 프롬프트를 입력하면 -- AI가 해당 형식에 맞춰 회의록을 재구성하여 제공한다 - -**지원 프롬프트 유형**: -- "1Page 요약": A4 1장 분량의 요약본 생성 -- "핵심 요약": 3-5개 핵심 포인트만 추출 -- "상세 보고서": 시간순 상세 기록 with 타임스탬프 -- "의사결정 중심": 결정 사항과 근거만 정리 -- "액션 아이템 중심": Todo와 담당자만 강조 -- "경영진 보고용": 임원진에게 보고할 형식으로 재구성 -- "커스텀 프롬프트": 사용자 정의 형식 - -**처리 결과**: -- 개선된 회의록이 생성됨 (새 버전) -- 원본 회의록 링크 유지 -- 생성 시간 및 프롬프트 기록 -- 다운로드 가능 (PDF, DOCX, MD) - ---- - -### 5. RAG 서비스 (2개) - 차별화 포인트 ✅ - -| ID | 기능명 | As a | I want | So that | 복잡도 | 차별화 | -|----|--------|------|--------|---------|--------|--------| -| UFR-RAG-010 | 전문용어 감지 | 회의록 작성자 | 전문용어가 자동으로 감지되고 맥락에 맞는 설명을 제공받고 싶다 | 업무 지식이 없어도 회의록을 정확히 작성하기 위해 | S/13 | ✅ | -| UFR-RAG-020 | 맥락 기반 용어 설명 | 회의록 작성자 | 관련 회의록과 업무 이력을 바탕으로 실용적인 설명을 제공받고 싶다 | 전문용어를 맥락에 맞게 이해하기 위해 | S/21 | ✅ | - -#### UFR-RAG-020: 맥락 기반 용어 설명 (핵심 차별화) - -**시나리오**: 맥락 기반 용어 설명 자동 제공 -- 전문용어가 감지된 상황에서 -- RAG 시스템이 관련 문서를 검색하면 -- 과거 회의록 및 업무 이력에서 맥락에 맞는 실용적인 설명이 생성되어 제공된다 - -**RAG 검색 수행**: -- 벡터 유사도 검색 - - 과거 회의록 검색 (동일 용어 사용 사례) - - 사내 문서 저장소 검색 (위키, 매뉴얼, 보고서) - - 업무 이력 검색 (프로젝트 문서, 이메일 등) -- 관련 문서 추출 (관련도 점수순) -- 최대 5개 문서 선택 - -**맥락 기반 설명 생성**: -- 간단한 정의 (1-2문장) -- 이 회의에서의 의미 (맥락 기반) -- 관련 프로젝트/이슈 연결 -- 과거 논의 요약 (언제, 누가, 어떻게 사용했는지) -- 참조 출처 링크 - -**처리 결과**: -- 맥락 기반 용어 설명이 생성됨 -- 툴팁 또는 사이드 패널로 표시 -- 관련 회의록 링크 (최대 3개) -- 사내 문서 링크 - -**차별화 포인트**: 단순 용어 설명이 아닌, 조직 내 실제 사용 맥락과 이력을 제공 - ---- - -### 6. Collaboration 서비스 (3개) - -| ID | 기능명 | As a | I want | So that | 복잡도 | -|----|--------|------|--------|---------|--------| -| UFR-COLLAB-010 | 회의록 수정 동기화 | 회의 참석자 | 회의록을 수정하고 실시간으로 다른 참석자와 동기화하고 싶다 | 회의록을 함께 검증하기 위해 | M/34 | -| UFR-COLLAB-020 | 충돌 해결 | 회의 참석자 | 충돌을 감지하고 해결하고 싶다 | 동시 수정 상황에서도 내용을 잃지 않기 위해 | M/21 | -| UFR-COLLAB-030 | 검증 완료 | 회의 참석자 | 주요 섹션을 검증하고 완료 표시를 하고 싶다 | 회의록의 정확성을 보장하기 위해 | M/8 | - ---- - -### 7. Todo 서비스 (2개) - 차별화 포인트 ✅ - -| ID | 기능명 | As a | I want | So that | 복잡도 | 차별화 | -|----|--------|------|--------|---------|--------|--------| -| UFR-TODO-010 | Todo 할당 | Todo 시스템 | Todo를 실시간으로 할당하고 회의록과 연결하고 싶다 | AI가 추출한 Todo를 담당자에게 전달하기 위해 | M/13 | ✅ | -| UFR-TODO-030 | Todo 완료 처리 | Todo 담당자 | Todo를 완료하고 회의록에 자동 반영하고 싶다 | 완료된 Todo를 처리하고 회의록에 반영하기 위해 | M/8 | ✅ | - -#### UFR-TODO-010: Todo 할당 (차별화 포인트) - -**시나리오**: Todo 실시간 할당 및 회의록 연결 -- AI가 Todo를 추출한 상황에서 -- 시스템이 Todo를 등록하고 담당자를 지정하면 -- Todo가 실시간으로 할당되고 회의록의 해당 위치와 연결되며 담당자에게 즉시 알림이 발송된다 - -**회의록 실시간 연결**: -- 회의록 해당 섹션에 Todo 뱃지 표시 -- Todo 클릭 시 Todo 상세 정보 표시 -- 양방향 연결 (Todo → 회의록, 회의록 → Todo) - -**처리 결과**: -- Todo가 할당됨 (Todo ID) -- 담당자 정보 -- 마감일 -- 회의록 연결 정보 (섹션 ID, 타임스탬프) -- 담당자에게 알림이 발송됨 -- 캘린더 등록 완료 - -**차별화 포인트**: Todo와 회의록의 강력한 연결, 원문 맥락 추적 가능 - ---- - -## 복잡도 분류 - -| 분류 | 개수 | 유저스토리 ID | -|------|------|--------------| -| **Small (S)** | 3개 | UFR-MEET-020, UFR-AI-040, UFR-RAG-010 | -| **Medium (M)** | 17개 | AFR-USER-010, UFR-MEET-010, UFR-MEET-030, UFR-MEET-040, UFR-MEET-050, UFR-MEET-060, UFR-STT-010, UFR-STT-020, UFR-AI-010, UFR-AI-020, UFR-AI-030, UFR-RAG-020, UFR-COLLAB-010, UFR-COLLAB-020, UFR-COLLAB-030, UFR-TODO-010, UFR-TODO-030 | -| **Large (L)** | 0개 | - | - -**총 Story Point**: -- Small: 3 × 5 = 15 SP -- Medium: 17개 (8-34점 범위) ≈ 평균 16 SP × 17 = 272 SP -- **합계**: 약 287 SP - ---- - -## 우선순위별 분류 - -### 높은 우선순위 (Must Have) -- AFR-USER-010 (인증) -- UFR-MEET-010 ~ 060 (회의 전체 플로우) -- UFR-STT-010, 020 (음성 인식) -- UFR-AI-010 (회의록 자동 작성) -- UFR-COLLAB-010 (실시간 동기화) - -### 중간 우선순위 (Should Have) -- UFR-AI-020 (Todo 자동 추출) ✅ 차별화 -- UFR-RAG-010, 020 (용어 설명) ✅ 차별화 -- UFR-TODO-010, 030 (Todo 관리) ✅ 차별화 -- UFR-COLLAB-020, 030 (충돌 해결, 검증) - -### 낮은 우선순위 (Nice to Have) -- UFR-AI-030 (회의록 개선) ✅ 차별화 -- UFR-AI-040 (관련 회의록 연결) ✅ 차별화 - ---- - -## 변경 이력 - -| 버전 | 날짜 | 변경 내용 | 작성자 | -|------|------|-----------|--------| -| 1.0 | 2025-01-20 | 유저스토리를 마크다운 표 형식으로 변환하여 작성 | Claude | - ---- - -**문서 끝** diff --git a/design/userstory.md b/design/userstory.md index 0c4852a..621af4a 100644 --- a/design/userstory.md +++ b/design/userstory.md @@ -34,8 +34,6 @@ 6. **Collaboration** - 실시간 동기화, 버전 관리, 충돌 해결 7. **Todo** - Todo 할당 및 관리, 진행 상황 추적, 회의록 실시간 연동 8. **Notification** - 알림 발송 및 리마인더 관리 -9. **Calendar** - 일정 생성 및 외부 캘린더 연동 -10. **Analytics** - 회의 효율성 분석, 패턴 분석, 개선 제안 (신규) --- @@ -43,13 +41,11 @@ ``` 1. User 서비스 -1) 사용자 인증 및 관리 -AFR-USER-010: [사용자관리] 시스템 관리자로서 | 나는, 서비스 보안을 위해 | 사용자 인증 및 권한 관리 기능을 원한다. -- 시나리오: 사용자 인증 및 권한 관리 - 사용자가 로그인을 시도한 상황에서 | 아이디와 비밀번호를 입력하면 | 인증이 완료되고 권한에 따라 서비스에 접근할 수 있다. - - [ ] 사용자 인증 (아이디, 비밀번호) - - [ ] JWT 토큰 기반 인증 - - [ ] 사용자 권한 관리 (관리자, 일반 사용자) +1) 사용자 인증 관리 +AFR-USER-010: [사용자관리] 시스템 관리자로서 | 나는, 서비스 보안을 위해 | 사용자 인증 기능을 원한다. +- 시나리오: 사용자 인증 관리 + 사용자가 로그인을 시도한 상황에서 | 사번과 비밀번호를 입력하면 | LDAP 연동을 통해 인증이 완료되고 권한에 따라 서비스에 접근할 수 있다. + - [ ] 사용자 인증 (사번, 비밀번호) - [ ] 세션 관리 - M/8 @@ -94,7 +90,6 @@ UFR-MEET-020: [템플릿선택] 회의록 작성자로서 | 나는, 회의록을 [처리 결과] - 선택된 템플릿으로 회의록 도구가 준비됨 - - 템플릿 ID와 설정 정보가 저장됨 - S/5 @@ -106,15 +101,16 @@ UFR-MEET-030: [회의시작] 회의록 작성자로서 | 나는, 회의를 시 [회의 시작 조건] - 예약된 회의가 존재함 - - 회의 시작 시간이 도래함 + - 회의 시작 시간 10분 전부터 회의 시작 버튼 활성화 - 회의록 작성자가 시작 권한을 가짐 + - 이미 시작된 회의일 경우, 진행중으로 표시 [처리 결과] - 회의 세션이 생성됨 (세션 ID) - 음성 녹음 준비 완료 - 참석자 목록 표시 - 회의 시작 시간 기록 - - 실시간 회의록 작성 화면 활성화 + - 실시간 회의록 주요 항목 추천 - M/8 @@ -137,7 +133,10 @@ UFR-MEET-040: [회의종료] 회의록 작성자로서 | 나는, 회의를 종 [처리 결과] - 회의가 종료됨 - 회의 통계 표시 - - 최종 회의록 확정 단계로 이동 + - 검증 완료 시 최종 회의록 확정 단계로 이동 + + [검증 미완료 시] + - 검증이 안된 항목이 있다면 회의록 히스토리 페이지에서 추후 수정 가능 - M/8 @@ -167,6 +166,84 @@ UFR-MEET-050: [최종확정] 회의록 작성자로서 | 나는, 회의록을 --- +UFR-MEET-045: [회의록상세조회] 회의록 작성자로서 | 나는, 지난 회의록의 상세 정보와 전체 내용을 | 한눈에 확인하고 싶다. +- 시나리오: 회의록 상세 정보 조회 + "내 회의록" 메뉴에서 특정 회의록을 클릭하면 | 해당 회의의 기본 정보와 섹션별 상세 내용이 표시되고 | 필요한 경우 수정, 공유, 다운로드 등의 작업을 수행할 수 있다. + + [회의 기본 정보 표시] + - 회의 제목 + - 회의 일시 (날짜 및 시간) + - 참석자 목록 (역할 구분: 주관자/참석자/불참자) + - 회의 장소 (온라인/오프라인) + - 사용된 템플릿 유형 + - 회의록 상태 (작성중/확정완료) + - 작성자 및 최종 수정 시간 + + [섹션별 상세 내용 표시] + - 각 섹션 구분 표시 (논의사항, 결정사항, Todo, 기타 등) + - 섹션별 검증 상태 표시 (검증완료 섹션은 체크 표시) + - Todo 항목: + - 담당자 이름 + - 마감일 + - 완료/미완료 상태 (시각적 구분) + - 우선순위 (있는 경우) + - 첨부파일 목록 및 다운로드 링크 + + [부가 기능] + - 회의록 수정 버튼 (수정 권한이 있는 경우만 표시) + - 회의록 공유 버튼 (공유 설정 화면으로 이동) + - 이전/다음 회의록으로 이동하는 네비게이션 + - 뒤로가기 버튼 (회의록 목록으로 복귀) + + [처리 결과] + - 모바일/태블릿 환경에서도 가독성 높은 레이아웃 + - 긴 내용은 적절한 단락 구분 및 여백 적용 + - 섹션별 접기/펼치기 기능 (선택사항) + - 페이지 로딩 시 스크롤 위치는 최상단 + + [권한별 표시] + - 조회 권한만 있는 경우: 수정 버튼 비활성화 + - 수정 권한이 있는 경우: 수정 버튼 활성화 + +- M/5 + +--- + +UFR-MEET-055: [회의록수정] 회의록 작성자로서 | 나는, 검증이 완료되지 않았거나 수정이 필요한 | 지난 회의록을 조회하고 수정하고 싶다. +- 시나리오: 지난 회의록 조회 및 수정 + 대시보드에서 "내 회의록" 메뉴를 클릭하면 | 작성한 회의록 목록이 표시되고 | 특정 회의록을 선택하여 수정할 수 있다. + + [회의록 목록 조회] + - 회의록 상태별 필터링: 전체 / 작성중 / 확정완료 + - 정렬 옵션: 최신순 / 회의일시순 / 제목순 + - 검색 기능: 회의 제목, 참석자, 키워드로 검색 + - 목록 표시 정보: + - 회의 제목 + - 회의 일시 + - 회의록 상태 (작성중/확정완료) + - 마지막 수정 시간 + - 검증 완료율 (작성중인 경우) + + [회의록 수정] + - 회의록 선택 시 상세 화면으로 이동 + - 상태에 따른 수정 가능 범위: + - 작성중: 모든 섹션 수정 가능 + - 확인완료: 회의록 생성자에게 수정 권한 승인요청 + - 수정 중 자동 저장 (30초 간격) + - 수정 이력 관리 (누가, 언제, 무엇을 수정했는지) + + [처리 결과] + - 수정 내용 즉시 반영 + - 수정 시간 업데이트 + - 확정완료 상태였던 경우 → 작성중 상태로 변경 + + [권한 제어] + - 본인이 작성한 회의록만 수정 가능 + - 검증완료 후 검증된 섹션 잠금 기능은 회의록 생성자만 가능 + - 모든 섹션이 검증완료일경우 회의록 상태를 확정완료로 변경 + +- M/13 + 3) 회의록 공유 UFR-MEET-060: [회의록공유] 회의록 작성자로서 | 나는, 회의 내용을 참석자들과 공유하기 위해 | 최종 회의록을 공유하고 싶다. - 시나리오: 회의록 공유 @@ -175,13 +252,13 @@ UFR-MEET-060: [회의록공유] 회의록 작성자로서 | 나는, 회의 내 [공유 설정] - 공유 대상: 참석자 전체 (기본) / 특정 참석자 선택 - 공유 권한: 읽기 전용 / 댓글 가능 / 편집 가능 - - 공유 방식: 이메일 / 슬랙 / 링크 복사 + - 공유 방식: 이메일 / 링크 복사 [처리 결과] - 공유 링크 생성 (고유 URL) - - 참석자에게 이메일/슬랙 알림 발송 + - 참석자에게 이메일 알림 발송 - 공유 시간 기록 - - 다음 회의 일정이 언급된 경우 캘린더에 자동 등록 (UFR-CAL-010 연동) + - 다음 회의 일정이 언급된 경우 캘린더에 자동 등록 [공유 링크 보안] - 링크 유효 기간 설정 (선택) @@ -191,61 +268,6 @@ UFR-MEET-060: [회의록공유] 회의록 작성자로서 | 나는, 회의 내 --- -UFR-MEET-070: [회의록대시보드] 회의록 작성자로서 | 나는, 회의 결과를 한눈에 파악하기 위해 | 회의록별 대시보드를 통해 핵심 정보를 조회하고 싶다. -- 시나리오: 회의록별 대시보드 조회 - 회의록이 확정된 상황에서 | 대시보드 탭을 클릭하면 | 핵심내용, 결정사항, Todo 진행상황, 참고자료가 요약되어 표시된다. - - [대시보드 구성 요소] - 1. 핵심내용 - - AI가 추출한 회의의 핵심 논의사항 (3-5개 포인트) - - 주요 키워드 태그 - - 회의 통계 (참석자 수, 회의 시간, 발언 횟수) - - 2. 결정사항 - - 회의에서 결정된 사항 목록 - - 각 결정사항별 결정자, 결정 시간 - - 결정 근거 및 배경 (간략) - - 3. Todo 진행상황 - - 할당된 Todo 목록 (UFR-TODO-010 연동) - - 각 Todo별 진행률 (0-100%) 표시 - - 상태별 필터링 (시작 전/진행 중/완료) - - 담당자별 그룹핑 - - 마감일 임박 알림 (3일 이내) - - 4. 참고자료 - - 관련 회의록 (UFR-AI-040 연동) - - 이전 회의록 링크 (시간순) - - 관련도 점수 표시 - - 업무 이력 (UFR-RAG-030 연동) - - 관련 프로젝트 문서 - - 이슈 트래커 링크 - - 사내 위키 페이지 - - [처리 결과] - - 대시보드가 생성됨 - - 각 섹션별 데이터 로딩 상태 표시 - - 실시간 업데이트 (Todo 진행상황) - - 섹션별 상세보기 링크 제공 - - [데이터 업데이트] - - Todo 진행상황: 실시간 업데이트 (UFR-TODO-020 연동) - - 참고자료: 매일 자동 업데이트 - - 핵심내용/결정사항: 회의록 수정 시 재생성 - - [Policy/Rule] - - 대시보드는 회의록 확정 후 자동 생성 - - Todo 진행상황은 실시간 반영 - - 모바일 최적화 (반응형 디자인) - - [비고] - - **차별화 포인트**: 회의 결과를 한눈에 파악할 수 있는 통합 뷰 제공 - - 관련 정보를 한 화면에서 접근 가능하여 업무 효율성 향상 - -- M/21 - ---- - 3. STT 서비스 (기본 기능) 1) 음성 인식 및 변환 UFR-STT-010: [음성녹음인식] 회의 참석자로서 | 나는, 발언 내용이 자동으로 기록되기 위해 | 음성이 실시간으로 녹음되고 인식되기를 원한다. @@ -255,10 +277,10 @@ UFR-STT-010: [음성녹음인식] 회의 참석자로서 | 나는, 발언 내용 [음성 녹음 처리] - 오디오 스트림 실시간 캡처 - 회의 ID와 연결 - - 음성 데이터 저장 (클라우드 스토리지) + - 음성 데이터 저장 (Azure 스토리지) [발언 인식 처리] - - AI 음성인식 엔진 연동 (Whisper, Google STT 등) + - AI 음성인식 엔진 연동 (Azure Speech 등) - 화자 자동 식별 - 참석자 목록 매칭 - 음성 특징 분석 @@ -378,7 +400,7 @@ UFR-AI-020: [Todo자동추출] 회의록 작성자로서 | 나는, 회의 후 [담당자 식별 실패 시] - 미지정 상태로 Todo 생성 - - 회의 주최자에게 수동 할당 요청 알림 + - 수동 할당 요청 알림 - M/21 @@ -413,7 +435,6 @@ UFR-AI-030: [회의록개선] 회의록 작성자로서 | 나는, 회의록을 - 개선된 회의록이 생성됨 (새 버전) - 원본 회의록 링크 유지 - 생성 시간 및 프롬프트 기록 - - 다운로드 가능 (PDF, DOCX, MD) [Policy/Rule] - 원본 회의록은 항상 보존 @@ -425,9 +446,9 @@ UFR-AI-030: [회의록개선] 회의록 작성자로서 | 나는, 회의록을 --- 4) 관련 회의록 자동 연결 (신규, 차별화 포인트) -UFR-AI-040: [관련회의록연결] 회의록 작성자로서 | 나는, 이전 회의 내용을 쉽게 참조하기 위해 | AI가 관련 있는 과거 회의록을 자동으로 찾아 연결해주기를 원한다. +UFR-AI-040: [관련회의록연결] 회의록 작성자로서 | 나는, 이전 회의 내용을 쉽게 참조하기 위해 | AI가 같은 폴더 내 관련 있는 과거 회의록을 자동으로 찾아 연결해주기를 원한다. - 시나리오: 관련 회의록 자동 연결 - 회의록이 작성되는 상황에서 | AI가 회의 주제와 내용을 분석하면 | 유사한 주제의 과거 회의록을 찾아 자동으로 연결한다. + 회의록이 작성되는 상황에서 | AI가 회의 주제와 내용을 분석하면 | 같은 폴더 내 유사한 주제의 과거 회의록을 찾아 자동으로 연결한다. [AI 분석 과정] - 현재 회의록 주제 및 키워드 추출 @@ -435,7 +456,7 @@ UFR-AI-040: [관련회의록연결] 회의록 작성자로서 | 나는, 이전 - 과거 회의록 DB에서 검색 - 주제 유사도 계산 - 관련도 점수 계산 (0-100%) - - 상위 5개 회의록 선정 + - 같은 폴더 내 상위 5개 회의록 선정 [연결 기준] - 주제 유사도 70% 이상 @@ -544,59 +565,11 @@ UFR-RAG-020: [맥락기반용어설명] 회의록 작성자로서 | 나는, 전 --- -2) 관련 문서 자동 연결 (신규, 차별화 포인트) -UFR-RAG-030: [관련문서연결] 회의록 작성자로서 | 나는, 회의 내용을 더 잘 이해하기 위해 | 관련된 사내 문서와 업무 이력이 자동으로 연결되기를 원한다. -- 시나리오: 관련 문서 자동 연결 - 회의록이 작성되는 상황에서 | RAG 시스템이 주제와 키워드를 분석하면 | 관련된 사내 문서와 업무 이력이 자동으로 검색되어 연결된다. - - [문서 검색 범위] - - 과거 회의록 - - 프로젝트 문서 - - 위키 페이지 - - 이메일 스레드 - - 보고서 및 기획서 - - 이슈 트래커 (Jira, Asana 등) - - [RAG 검색 수행] - - 회의 주제 및 키워드 추출 - - 벡터 유사도 검색 - - 관련도 점수 계산 (0-100%) - - 문서 타입별 상위 3개 선정 - - [연결 기준] - - 주제 유사도 70% 이상 - - 키워드 3개 이상 일치 - - 최근 3개월 이내 문서 우선 - - 동일 프로젝트/팀 문서 가중치 - - [처리 결과] - - 관련 문서 목록 생성 - - 각 문서별 정보 - - 제목 - - 문서 타입 - - 작성자 - - 작성일 - - 관련도 점수 - - 핵심 내용 요약 (2-3줄) - - 회의록 하단에 "관련 문서" 섹션 자동 추가 - - 클릭 시 해당 문서로 이동 또는 미리보기 - - [Policy/Rule] - - 관련도 70% 이상만 자동 연결 - - 문서 타입별 최대 3개까지 표시 - - [비고] - - **차별화 포인트**: 회의록만 보는 것이 아니라 관련 업무 이력 전체를 통합 제공 - -- S/13 - ---- - 6. Collaboration 서비스 1) 실시간 협업 UFR-COLLAB-010: [회의록수정동기화] 회의 참석자로서 | 나는, 회의록을 함께 검증하기 위해 | 회의록을 수정하고 실시간으로 다른 참석자와 동기화하고 싶다. - 시나리오: 회의록 실시간 수정 및 동기화 - 회의록이 작성된 상황에서 | 참석자가 회의록 내용을 수정하면 | 수정 사항이 버전 관리되고 웹소켓을 통해 모든 참석자에게 즉시 동기화된다. + 회의록 초안이 작성된 상황에서 | 참석자가 회의록 내용을 수정하면 | 수정 사항이 버전 관리되고 웹소켓을 통해 모든 참석자에게 즉시 동기화된다. [회의록 수정 처리] - 수정 내용 검증 @@ -615,7 +588,7 @@ UFR-COLLAB-010: [회의록수정동기화] 회의 참석자로서 | 나는, 회 - 웹소켓을 통해 수정 델타 전송 - 전체 내용이 아닌 변경 부분만 전송 (효율성) - 모든 참석자 화면에 실시간 반영 - - 수정자 표시 (아바타, 이름) + - 수정자 표시 (이름) - 수정 영역 하이라이트 (3초간) [처리 결과] @@ -623,6 +596,7 @@ UFR-COLLAB-010: [회의록수정동기화] 회의 참석자로서 | 나는, 회 - 수정 사항이 동기화됨 - 동기화 시간 - 영향받은 참석자 목록 + - 수정 완료될 때마다 수정된 내용이 메일로 알림이 발송된다. (알림 여부 설정 가능) [Policy/Rule] - 회의록 수정 시 웹소켓을 통해 모든 참석자에게 즉시 동기화 @@ -681,9 +655,10 @@ UFR-COLLAB-030: [검증완료] 회의 참석자로서 | 나는, 회의록의 정 - 미검증 → 검증 중 → 검증 완료 [섹션 잠금 기능] + - 회의 생성자만 가능 - 주요 섹션 검증 완료 시 잠금 가능 (선택) - 잠긴 섹션은 추가 수정 불가 - - 잠금 해제는 검증자 또는 회의 주최자만 가능 + - 회의 생성자가 잠그면 검증 완료로 표시 [처리 결과] - 검증이 완료됨 @@ -692,6 +667,7 @@ UFR-COLLAB-030: [검증완료] 회의 참석자로서 | 나는, 회의록의 정 - 완료 시간 - 검증 완료 상태 실시간 동기화 - 검증 배지 표시 (체크 아이콘) + - 검증 완료 시 전체 메일로 알림이 발송된다. [Policy/Rule] - 주요 섹션 검증 완료 시 해당 섹션 잠금 가능 @@ -714,7 +690,6 @@ UFR-TODO-010: [Todo할당] Todo 시스템으로서 | 나는, AI가 추출한 Tod - 마감일 (언급된 경우 자동 설정, 없으면 수동 설정) - 우선순위 (높음/보통/낮음) - 관련 회의록 링크 (섹션 위치 포함) - - 원문 발언 링크 (타임스탬프 포함) [회의록 실시간 연결] - 회의록 해당 섹션에 Todo 뱃지 표시 @@ -724,12 +699,10 @@ UFR-TODO-010: [Todo할당] Todo 시스템으로서 | 나는, AI가 추출한 Tod [알림 발송] - 담당자에게 즉시 알림 - 이메일 - - 슬랙 (연동된 경우) - 알림 내용 - Todo 내용 - 마감일 - 회의록 링크 (해당 섹션으로 바로 이동) - - 원문 발언 링크 [캘린더 연동] - 마감일이 있는 경우 캘린더에 자동 등록 @@ -755,53 +728,9 @@ UFR-TODO-010: [Todo할당] Todo 시스템으로서 | 나는, AI가 추출한 Tod --- -UFR-TODO-020: [Todo진행상황업데이트] Todo 담당자로서 | 나는, Todo 진행 상황을 공유하고 회의록에 반영하기 위해 | 진행률을 업데이트하고 상태를 변경하고 싶다. -- 시나리오: Todo 진행 상황 업데이트 및 회의록 자동 반영 - 할당된 Todo가 있는 상황에서 | 담당자가 진행률과 상태를 입력하면 | 진행 상황이 저장되고 연결된 회의록에 실시간으로 반영되며 회의 주최자에게 알림이 발송된다. - - [진행 상황 입력] - - 진행률: 0-100% (슬라이더 또는 직접 입력) - - 상태: 시작 전 / 진행 중 / 완료 - - 메모: 진행 상황 설명 (선택) - - [진행 상황 저장] - - 업데이트 시간 기록 - - 진행률 히스토리 저장 - - 상태 변경 이력 저장 - - [회의록 실시간 반영] - - 연결된 회의록의 Todo 섹션 자동 업데이트 - - 진행률 표시 (프로그레스 바) - - 상태 배지 업데이트 (시작 전/진행 중/완료) - - 마지막 업데이트 시간 표시 - - 담당자 메모 표시 (있는 경우) - - [알림 발송] - - 회의 주최자에게 진행 상황 알림 - - 진행률이 50%, 100%에 도달하면 자동 알림 - - [처리 결과] - - Todo 진행 상황이 업데이트됨 - - 업데이트 시간 - - 진행률 (%) - - 상태 (시작 전/진행 중/완료) - - 회의록에 진행 상황이 실시간 반영됨 - - 반영 시간 기록 - - [Policy/Rule] - - Todo 진행 상황 업데이트 시 회의록에 즉시 반영 - - 진행률 50%, 100% 도달 시 자동 알림 - - [비고] - - **차별화 포인트**: Todo 진행 상황이 회의록에 실시간 반영되어 추적 용이 - -- M/5 - ---- - UFR-TODO-030: [Todo완료처리] Todo 담당자로서 | 나는, 완료된 Todo를 처리하고 회의록에 반영하기 위해 | Todo를 완료하고 회의록에 자동 반영하고 싶다. - 시나리오: Todo 완료 처리 및 회의록 자동 반영 - Todo 작업이 완료된 상황에서 | 담당자가 완료 버튼을 클릭하면 | Todo가 완료 상태로 변경되고 연결된 회의록에 완료 상태가 실시간으로 반영되며 회의 주최자에게 알림이 발송된다. + Todo 작업이 완료된 상황에서 | 담당자가 완료 버튼을 클릭하면 | Todo가 완료 상태로 변경되고 연결된 회의록에 완료 상태가 실시간으로 반영된다. [완료 처리] - 완료 시간 자동 기록 @@ -816,7 +745,7 @@ UFR-TODO-030: [Todo완료처리] Todo 담당자로서 | 나는, 완료된 Todo - 완료자 정보 표시 [알림 발송] - - 회의 주최자에게 완료 알림 + - 완료 알림 - 모든 Todo 완료 시 전체 완료 알림 [처리 결과] @@ -829,234 +758,11 @@ UFR-TODO-030: [Todo완료처리] Todo 담당자로서 | 나는, 완료된 Todo [Policy/Rule] - Todo 완료 시 회의록에 완료 상태 즉시 반영 - - 모든 Todo 완료 시 회의 주최자에게 완료 알림 + - 모든 Todo 완료 시 완료 알림 [비고] - **차별화 포인트**: Todo 완료가 회의록에 실시간 반영되어 회의 결과 추적 용이 - M/8 ---- - -2) 회의 중 실시간 Todo 생성 (신규, 차별화 포인트) -UFR-TODO-040: [실시간Todo생성] 회의 참석자로서 | 나는, 회의 중 논의된 액션 아이템을 즉시 기록하기 위해 | 회의 진행 중 실시간으로 Todo를 생성하고 회의록과 연결하고 싶다. -- 시나리오: 회의 중 실시간 Todo 생성 - 회의가 진행 중인 상황에서 | 참석자가 "Todo 추가" 버튼을 클릭하고 내용을 입력하면 | Todo가 즉시 생성되고 현재 회의록 위치와 연결되며 타임스탬프가 기록된다. - - [실시간 Todo 생성] - - Todo 내용 입력 (필수) - - 담당자 선택 (필수) - - 마감일 설정 (선택) - - 우선순위 설정 (선택) - - 현재 회의 시간 자동 기록 (타임스탬프) - - [회의록 자동 연결] - - 현재 작성 중인 회의록 섹션과 자동 연결 - - Todo 생성 시점의 타임스탬프 저장 - - 회의록에 Todo 뱃지 자동 추가 - - 음성 녹음 링크 연결 (해당 시간대) - - [실시간 동기화] - - 모든 참석자 화면에 즉시 표시 - - Todo 추가 알림 (인앱) - - 담당자에게 즉시 알림 발송 - - [처리 결과] - - Todo가 생성됨 (Todo ID) - - Todo 내용, 담당자, 마감일 - - 회의록 연결 정보 (섹션 ID, 타임스탬프) - - 생성 시간 및 생성자 - - 모든 참석자에게 동기화됨 - - [Policy/Rule] - - 회의 중 생성된 Todo는 회의록과 자동 연결 - - 담당자에게 즉시 알림 발송 - - [비고] - - **차별화 포인트**: 회의 중 실시간 Todo 생성으로 액션 아이템 누락 방지 - -- S/8 - ---- - -8. Notification 서비스 -1) 알림 관리 -UFR-NOTI-010: [알림리마인더] 회의 참석자로서 | 나는, 중요한 일정을 놓치지 않기 위해 | 회의 및 Todo 관련 알림과 리마인더를 받고 싶다. -- 시나리오 1: 회의 알림 - 회의가 예약된 상황에서 | 회의 시작 30분 전이 되면 | 참석자에게 리마인더가 자동 발송된다. - - [회의 알림 유형] - - 회의 초대: 회의 예약 시 - - 회의 시작 리마인더: 30분 전 - - 회의록 공유: 회의 종료 후 - -- 시나리오 2: Todo 알림 - Todo가 할당된 상황에서 | 마감일 3일 전이 되면 | 담당자에게 리마인더가 자동 발송된다. - - [Todo 알림 유형] - - Todo 할당: 할당 즉시 - - 마감일 3일 전 리마인더 - - 마감일 당일 리마인더 - - 마감일 경과 긴급 알림 (미완료 시) - - Todo 완료: 완료 시 - - [알림 채널] - - 이메일 (기본) - - 슬랙 (연동 시) - - 인앱 알림 - - [알림 설정] - - 알림 채널 선택 - - 알림 시간 설정 - - 알림 끄기/켜기 - - [처리 결과] - - 알림이 발송됨 (알림 ID) - - 알림 대상 (이메일 주소, 슬랙 ID) - - 알림 내용 - - 발송 시간 - - 발송 채널 - - 발송 상태 (성공/실패) - - [Policy/Rule] - - 회의 시작 30분 전 리마인더 자동 발송 - - 마감일 3일 전 자동 리마인더 발송 - - 마감일 당일 미완료 시 긴급 알림 발송 - -- M/13 - ---- - -9. Calendar 서비스 -1) 일정 관리 -UFR-CAL-010: [일정연동] 회의록 작성자로서 | 나는, 일정을 통합 관리하기 위해 | 회의 및 다음 회의 일정을 외부 캘린더에 자동으로 연동하고 싶다. -- 시나리오 1: 회의 일정 자동 등록 - 회의가 예약된 상황에서 | 시스템이 일정 동기화를 요청하면 | 회의 일정이 Google Calendar, Outlook 등 외부 캘린더에 자동으로 등록된다. - - [일정 등록 정보] - - 회의 제목 - - 날짜 및 시간 - - 장소 - - 참석자 목록 - - 회의록 링크 (메모) - -- 시나리오 2: 다음 회의 일정 연동 - 회의록에서 다음 회의 일정이 언급된 상황에서 | 시스템이 자동으로 감지하면 | 다음 회의 일정이 캘린더에 자동으로 생성된다. - - [자동 감지 키워드] - - "다음 회의: ~" - - "~에 다시 모이기로 함" - - "후속 회의 일정: ~" - - [처리 결과] - - 일정이 캘린더에 연동됨 (일정 ID) - - 연동 상태 (성공/실패) - - 캘린더 종류 (Google Calendar, Outlook) - - 연동 시간 - - [지원 캘린더] - - Google Calendar - - Microsoft Outlook - - Apple Calendar - - [Policy/Rule] - - 다음 회의 일정이 언급되면 자동으로 캘린더에 등록 - -- S/13 - ---- - -10. Analytics 서비스 (신규, 차별화 포인트) -1) 회의 효율성 분석 -UFR-ANAL-010: [회의효율성분석] 회의 주최자로서 | 나는, 회의를 개선하기 위해 | 회의 효율성을 분석하고 개선 제안을 받고 싶다. -- 시나리오: 회의 효율성 분석 및 개선 제안 - 회의가 종료된 상황에서 | Analytics 시스템이 회의 데이터를 분석하면 | 회의 효율성 점수와 구체적인 개선 제안이 제공된다. - - [분석 지표] - - 회의 시간 준수율 (예정 시간 대비 실제 시간) - - 참석자 참여도 (발언 분포, 침묵 시간) - - 안건 소화율 (계획된 안건 대비 논의된 안건) - - 의사결정 효율성 (결정 사항 수 / 회의 시간) - - Todo 생성률 (액션 아이템 명확성) - - [AI 분석 과정] - - 회의 통계 데이터 수집 - - 과거 유사 회의와 비교 - - 업계 벤치마크 대조 - - 비효율 패턴 감지 - - 너무 긴 회의 (2시간 이상) - - 참여도 불균형 (1명이 50% 이상 발언) - - 안건 없이 진행 - - 결정 사항 없음 - - Todo 미생성 - - [개선 제안 생성] - - 구체적인 개선 사항 제시 - - "회의 시간을 30분 단축 권장" - - "참석자 A의 발언 시간이 과도합니다. 타임박스 적용 권장" - - "안건을 사전에 공유하여 준비도를 높이세요" - - "결정 사항이 없습니다. 회의 목적을 재검토하세요" - - [처리 결과] - - 회의 효율성 점수 (0-100점) - - 각 지표별 점수 및 벤치마크 비교 - - 개선 제안 리스트 (우선순위순) - - 다음 회의 시 적용할 액션 아이템 - - [Policy/Rule] - - 모든 회의 종료 시 자동 분석 - - 효율성 점수 70점 미만 시 개선 알림 - - [비고] - - **차별화 포인트**: 회의 효율성을 정량적으로 측정하고 실질적 개선 제안 제공 - -- M/21 - ---- - -2) 회의 패턴 분석 및 안건 추천 (신규, 차별화 포인트) -UFR-ANAL-020: [회의패턴분석] 회의 주최자로서 | 나는, 더 나은 회의를 준비하기 위해 | 과거 회의 패턴을 분석하고 안건을 추천받고 싶다. -- 시나리오: 회의 패턴 분석 및 안건 추천 - 새로운 회의를 예약하는 상황에서 | Analytics 시스템이 과거 유사 회의를 분석하면 | 회의 패턴 인사이트와 안건 추천이 제공된다. - - [패턴 분석] - - 회의 유형 분류 (주간 회의, 프로젝트 회의, 의사결정 회의 등) - - 주기성 분석 (주간, 격주, 월간) - - 참석자 패턴 (핵심 멤버, 선택 멤버) - - 주요 논의 주제 추출 - - 평균 회의 시간 및 최적 시간대 - - [안건 추천] - - 과거 회의록 분석 - - 미해결 이슈 추출 - - 후속 논의 필요 사항 식별 - - 주기적 확인 사항 (KPI, 진행 상황) - - 관련 프로젝트/업무 이력 검토 - - 추천 안건 생성 - - 안건 제목 - - 논의 배경 (과거 회의록 링크) - - 예상 소요 시간 - - [최적 회의 구성 제안] - - 추천 참석자 (과거 패턴 기반) - - 추천 회의 시간 (참석자 캘린더 분석) - - 추천 회의 길이 (안건 수 기반) - - [처리 결과] - - 회의 패턴 인사이트 - - 추천 안건 리스트 (최대 5개) - - 최적 회의 구성 제안 - - 과거 유사 회의 링크 - - [Policy/Rule] - - 회의 예약 시 자동으로 패턴 분석 및 추천 제공 - - 사용자가 수락/거부 가능 - - [비고] - - **차별화 포인트**: 과거 회의 데이터를 활용한 지능형 회의 준비 지원 - -- M/13 - ---- - -``` +--- \ No newline at end of file diff --git a/design-last/userstory_list.md b/design/userstory_list.md similarity index 100% rename from design-last/userstory_list.md rename to design/userstory_list.md diff --git a/design/구현방안-맥락기반용어설명.md b/design/구현방안-맥락기반용어설명.md deleted file mode 100644 index 6032fd1..0000000 --- a/design/구현방안-맥락기반용어설명.md +++ /dev/null @@ -1,976 +0,0 @@ -# 맥락기반 용어설명 구현방안 - -## 문서 정보 -- **작성일**: 2025-01-20 -- **작성자**: AI Specialist 박서연, Backend Developer 이준호/이동욱, Architect 홍길동 -- **버전**: 2.0 (하이브리드형 - 단계별 확장 방식) -- **상태**: 최종 승인 - ---- - -## 목차 -1. [개요](#개요) -2. [아키텍처 설계](#아키텍처-설계) -3. [데이터 수집 및 정제](#데이터-수집-및-정제) -4. [벡터라이징 전략](#벡터라이징-전략) -5. [Claude API 호출 구조](#claude-api-호출-구조) -6. [단계별 구현 로드맵](#단계별-구현-로드맵) -7. [성능 및 비용 최적화](#성능-및-비용-최적화) -8. [품질 검증 기준](#품질-검증-기준) -9. [운영 및 모니터링](#운영-및-모니터링) - ---- - -## 개요 - -### 목적 -회의록 작성자가 업무 지식이 없어도, AI가 **맥락에 맞는 실용적인 용어 설명**을 자동으로 제공하여 정확한 회의록 작성을 지원합니다. - -### 핵심 차별화 포인트 -- ❌ 단순 용어 정의 (Wikipedia 스타일) -- ✅ **조직 내 실제 사용 맥락** 제공 -- ✅ **관련 회의록 및 프로젝트 연결** -- ✅ **과거 논의 요약** (언제, 누가, 어떻게 사용했는지) - -### 관련 유저스토리 -- **UFR-RAG-010**: 전문용어 자동 감지 -- **UFR-RAG-020**: 맥락 기반 용어 설명 생성 - ---- - -## 아키텍처 설계 - -### 전체 아키텍처 (최종 목표) - -``` -┌─────────────────────────────────────────────────────────┐ -│ 회의록 작성 중 │ -│ (전문용어 "RAG" 감지) │ -└──────────────────┬──────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────┐ -│ RAG 서비스 (Node.js) │ -│ ┌───────────────────────────────────────────────────┐ │ -│ │ 1. 용어 감지 엔진 │ │ -│ │ - 용어 사전 매칭 (Trie 자료구조) │ │ -│ │ - 신뢰도 계산 (0-100%) │ │ -│ └───────────────────────────────────────────────────┘ │ -│ ┌───────────────────────────────────────────────────┐ │ -│ │ 2. Redis 캐시 조회 │ │ -│ │ Key: term:{용어명}:{회의ID} │ │ -│ │ TTL: 자주 쓰이는 용어 7일, 드문 용어 1일 │ │ -│ └───────────────────────────────────────────────────┘ │ -│ ▼ (캐시 미스) │ -│ ┌───────────────────────────────────────────────────┐ │ -│ │ 3. 벡터 검색 (Pinecone) │ │ -│ │ - Query Embedding (OpenAI text-embedding-3) │ │ -│ │ - 하이브리드 검색 (벡터 + 키워드) │ │ -│ │ - Top 5 관련 문서 추출 │ │ -│ └───────────────────────────────────────────────────┘ │ -│ ▼ │ -│ ┌───────────────────────────────────────────────────┐ │ -│ │ 4. Claude API 호출 │ │ -│ │ - 프롬프트: System + User + Few-shot │ │ -│ │ - 응답: JSON {definition, context, related} │ │ -│ └───────────────────────────────────────────────────┘ │ -│ ▼ │ -│ ┌───────────────────────────────────────────────────┐ │ -│ │ 5. 응답 캐싱 (Redis) │ │ -│ │ - 다음 요청 시 즉시 반환 │ │ -│ └───────────────────────────────────────────────────┘ │ -└─────────────────────────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────┐ -│ 사용자에게 설명 표시 │ -│ - 간단한 정의 (1-2문장) │ -│ - 맥락 기반 설명 │ -│ - 관련 회의록 링크 (최대 3개) │ -│ - 과거 사용 사례 │ -└─────────────────────────────────────────────────────────┘ -``` - -### 기술 스택 - -| 계층 | 기술 | 선택 이유 | -|------|------|-----------| -| **임베딩 모델** | OpenAI text-embedding-3-large | 높은 정확도 (1536 차원), 안정적 API | -| **벡터 DB** | Pinecone | 관리형 서비스, 빠른 검색, Kubernetes 호환 | -| **LLM** | Claude 3.5 Sonnet | 긴 컨텍스트, 한국어 성능 우수, JSON 응답 안정적 | -| **캐시** | Redis | 빠른 응답, TTL 지원, 분산 캐시 가능 | -| **메시지 큐** | RabbitMQ | 배치 작업 비동기 처리 | -| **오케스트레이션** | Kubernetes | 스케일링, 배포 자동화 | - ---- - -## 데이터 수집 및 정제 - -### 1. 데이터 수집 범위 - -#### Phase 1 (2주): 회의록만 -``` -회의록 DB (Meeting 서비스) - ├─ meeting_id - ├─ title - ├─ content (Markdown) - ├─ participants - ├─ date - └─ project_id -``` - -#### Phase 2 (4주): 위키 추가 -``` -사내 위키 (Confluence, Notion 등) - ├─ page_id - ├─ title - ├─ content - ├─ author - ├─ last_updated - └─ tags -``` - -#### Phase 3 (6주): 프로젝트 문서 + 이메일 -``` -프로젝트 문서 (Google Drive, SharePoint) - ├─ doc_id - ├─ title - ├─ content - ├─ project_id - └─ created_at - -이메일 (Outlook, Gmail) - ├─ email_id - ├─ subject - ├─ body (HTML → Plain Text 변환) - ├─ sender - └─ date -``` - -### 2. 데이터 정제 파이프라인 - -```mermaid -graph LR - A[원본 수집] --> B[전처리] - B --> C[메타데이터 추가] - C --> D[벡터화] - D --> E[Pinecone 저장] - - B --> B1[불용어 제거] - B --> B2[토큰화] - B --> B3[정규화] - - C --> C1[날짜] - C --> C2[참석자/작성자] - C --> C3[프로젝트] - C --> C4[부서] -``` - -#### 전처리 상세 - -**1) 불용어 제거** -```python -STOPWORDS = [ - '그', '저', '것', '수', '등', '들', '및', '때문', '위해', '통해', - '하지만', '그러나', '따라서', '또한', '즉', '예를 들어' -] - -def remove_stopwords(text): - tokens = text.split() - return ' '.join([t for t in tokens if t not in STOPWORDS]) -``` - -**2) 토큰화 (한국어)** -```python -from konlpy.tag import Okt - -okt = Okt() - -def tokenize_korean(text): - return okt.morphs(text, stem=True) -``` - -**3) 정규화** -```python -import re - -def normalize(text): - # 이메일 제거 - text = re.sub(r'\S+@\S+', '[EMAIL]', text) - # URL 제거 - text = re.sub(r'http\S+', '[URL]', text) - # 특수문자 제거 (단, -_ 유지) - text = re.sub(r'[^\w\s-_]', '', text) - # 공백 정리 - text = re.sub(r'\s+', ' ', text) - return text.strip() -``` - -### 3. 메타데이터 설계 - -```json -{ - "id": "doc_12345", - "content": "RAG 시스템은 Retrieval-Augmented Generation의 약자로...", - "metadata": { - "source": "meeting", // meeting | wiki | doc | email - "title": "프로젝트 회의", - "date": "2025-01-20T14:00:00Z", - "participants": ["김민준", "박서연", "이준호"], - "project_id": "proj_001", - "project_name": "회의록 시스템", - "department": "개발팀", - "tags": ["RAG", "AI", "회의록"], - "language": "ko" - } -} -``` - ---- - -## 벡터라이징 전략 - -### 1. Chunking 전략 - -**목표**: 회의록/문서를 의미 있는 단위로 분할하여 검색 정확도 향상 - -```python -def chunk_text(text, chunk_size=500, overlap=50): - """ - 텍스트를 chunk로 분할 - - Args: - text: 원본 텍스트 - chunk_size: 청크 크기 (토큰 수) - overlap: 청크 간 중복 크기 - - Returns: - List[str]: 청크 리스트 - """ - tokens = tokenize_korean(text) - chunks = [] - - for i in range(0, len(tokens), chunk_size - overlap): - chunk = tokens[i:i + chunk_size] - chunks.append(' '.join(chunk)) - - return chunks -``` - -**Chunking 전략 비교** - -| 방식 | 크기 | Overlap | 장점 | 단점 | -|------|------|---------|------|------| -| **고정 크기** | 500 토큰 | 50 토큰 | 단순, 빠름 | 문맥 끊김 가능 | -| **문단 기반** | 가변 | 0 | 자연스러운 구분 | 크기 불균등 | -| **문장 기반** | 가변 | 1 문장 | 의미 보존 | 너무 작을 수 있음 | -| **하이브리드** | 500 토큰 | 50 토큰 + 문단 경계 | 균형잡힘 | 복잡함 | - -**선택**: **하이브리드 방식** (Phase 2 이후 적용) - -### 2. Embedding 생성 - -```python -import openai - -def generate_embedding(text, model="text-embedding-3-large"): - """ - OpenAI API로 임베딩 생성 - - Returns: - List[float]: 1536 차원 벡터 - """ - response = openai.embeddings.create( - input=text, - model=model - ) - return response.data[0].embedding -``` - -**비용 계산**: -- text-embedding-3-large: $0.00013 / 1K tokens -- 예상 월 비용: 500 회의록 × 2K tokens × $0.00013 = $0.13 - -### 3. Pinecone 저장 - -```python -import pinecone - -# 초기화 -pinecone.init(api_key="YOUR_API_KEY", environment="us-west1-gcp") -index = pinecone.Index("meeting-rag") - -def upsert_to_pinecone(doc_id, embedding, metadata): - """ - Pinecone에 벡터 저장 - """ - index.upsert(vectors=[{ - "id": doc_id, - "values": embedding, - "metadata": metadata - }]) -``` - -**Pinecone 설정**: -- Index: meeting-rag -- Dimension: 1536 -- Metric: cosine -- Replicas: 1 (Phase 1), 2 (Phase 3, HA) -- Pods: p1.x1 (Phase 1), p1.x2 (Phase 2+) - ---- - -## Claude API 호출 구조 - -### 1. 프롬프트 설계 - -#### System Prompt -``` -당신은 조직 내 전문용어를 쉽게 설명하는 전문가입니다. -- 사내 회의록, 위키, 프로젝트 문서를 기반으로 실용적인 설명을 제공합니다. -- 단순 정의가 아닌, 조직에서 실제로 어떻게 사용되는지 맥락을 포함합니다. -- 과거 논의 내용을 요약하여 제공합니다. - -응답 형식은 반드시 JSON으로 작성하세요: -{ - "definition": "간단한 정의 (1-2문장)", - "context": "이 회의에서의 의미 (맥락 기반 설명)", - "usage_examples": ["실제 사용 사례 1", "사용 사례 2"], - "related_projects": ["관련 프로젝트 1", "프로젝트 2"], - "past_discussions": [ - {"date": "2025-01-15", "meeting": "프로젝트 회의", "summary": "RAG 시스템 도입 결정"} - ], - "references": ["doc_id_1", "doc_id_2", "doc_id_3"] -} -``` - -#### User Prompt (Few-shot Learning) -``` -아래는 검색된 관련 문서들입니다: - ---- -문서 1 (회의록, 2025-01-15): -제목: 프로젝트 회의 -내용: RAG 시스템을 도입하기로 결정했습니다. Retrieval-Augmented Generation은 문서 검색과 생성을 결합한 AI 기술입니다... - -문서 2 (위키, 2025-01-10): -제목: AI 기술 가이드 -내용: RAG는 벡터 DB를 활용하여 관련 문서를 찾고, LLM이 이를 기반으로 답변을 생성하는 방식입니다... - -문서 3 (프로젝트 문서, 2025-01-05): -제목: 회의록 시스템 설계서 -내용: 맥락 기반 용어 설명 기능에 RAG 시스템을 적용합니다... ---- - -현재 회의 맥락: -- 회의: "주간 스크럼" -- 날짜: 2025-01-20 -- 참석자: 김민준, 박서연, 이준호 -- 프로젝트: "회의록 시스템" - -용어: "RAG" - -위 정보를 바탕으로 "RAG"에 대한 설명을 JSON 형식으로 작성해주세요. - -예시: -{ - "definition": "Retrieval-Augmented Generation의 약자로, 문서 검색과 AI 생성을 결합한 기술입니다.", - "context": "우리 팀은 회의록 시스템에 RAG를 적용하여 과거 회의록과 사내 문서를 검색하고, 맥락에 맞는 용어 설명을 자동 생성합니다.", - "usage_examples": [ - "회의 중 전문용어가 나오면 RAG 시스템이 관련 문서를 찾아 설명을 제공합니다", - "신입사원도 업무 지식 없이 정확한 회의록을 작성할 수 있습니다" - ], - "related_projects": ["회의록 시스템", "AI 자동화 프로젝트"], - "past_discussions": [ - {"date": "2025-01-15", "meeting": "프로젝트 회의", "summary": "RAG 시스템 도입 결정"}, - {"date": "2025-01-10", "meeting": "기술 세미나", "summary": "RAG 아키텍처 소개"} - ], - "references": ["doc_12345", "doc_12346", "doc_12347"] -} -``` - -### 2. API 호출 코드 - -```typescript -import Anthropic from '@anthropic-ai/sdk'; - -const anthropic = new Anthropic({ - apiKey: process.env.CLAUDE_API_KEY, -}); - -interface TermExplanation { - definition: string; - context: string; - usage_examples: string[]; - related_projects: string[]; - past_discussions: Array<{ - date: string; - meeting: string; - summary: string; - }>; - references: string[]; -} - -async function explainTerm( - term: string, - relatedDocs: any[], - meetingContext: any -): Promise { - - const systemPrompt = `당신은 조직 내 전문용어를 쉽게 설명하는 전문가입니다...`; - - const userPrompt = buildUserPrompt(term, relatedDocs, meetingContext); - - const response = await anthropic.messages.create({ - model: 'claude-3-5-sonnet-20241022', - max_tokens: 2000, - temperature: 0.3, // 일관된 응답을 위해 낮은 temperature - system: systemPrompt, - messages: [ - { - role: 'user', - content: userPrompt - } - ] - }); - - // JSON 파싱 - const content = response.content[0].text; - const jsonMatch = content.match(/\{[\s\S]*\}/); - - if (!jsonMatch) { - throw new Error('Invalid JSON response from Claude'); - } - - return JSON.parse(jsonMatch[0]); -} - -function buildUserPrompt(term: string, docs: any[], context: any): string { - const docsSummary = docs.map((doc, idx) => { - return `문서 ${idx + 1} (${doc.metadata.source}, ${doc.metadata.date}): -제목: ${doc.metadata.title} -내용: ${doc.content.substring(0, 500)}...`; - }).join('\n\n'); - - return `아래는 검색된 관련 문서들입니다: - ---- -${docsSummary} ---- - -현재 회의 맥락: -- 회의: "${context.meeting_title}" -- 날짜: ${context.date} -- 참석자: ${context.participants.join(', ')} -- 프로젝트: "${context.project_name}" - -용어: "${term}" - -위 정보를 바탕으로 "${term}"에 대한 설명을 JSON 형식으로 작성해주세요. - -예시: -{ - "definition": "...", - "context": "...", - ... -}`; -} -``` - -### 3. API 요청/응답 예시 - -#### 요청 (Request) -```json -{ - "model": "claude-3-5-sonnet-20241022", - "max_tokens": 2000, - "temperature": 0.3, - "system": "당신은 조직 내 전문용어를 쉽게 설명하는 전문가입니다...", - "messages": [ - { - "role": "user", - "content": "아래는 검색된 관련 문서들입니다...\n용어: \"RAG\"" - } - ] -} -``` - -#### 응답 (Response) -```json -{ - "id": "msg_01ABC123", - "type": "message", - "role": "assistant", - "content": [ - { - "type": "text", - "text": "{\n \"definition\": \"Retrieval-Augmented Generation의 약자로, 문서 검색과 AI 생성을 결합한 기술입니다.\",\n \"context\": \"우리 팀은 회의록 시스템에 RAG를 적용하여 과거 회의록과 사내 문서를 검색하고, 맥락에 맞는 용어 설명을 자동 생성합니다.\",\n \"usage_examples\": [\n \"회의 중 전문용어가 나오면 RAG 시스템이 관련 문서를 찾아 설명을 제공합니다\",\n \"신입사원도 업무 지식 없이 정확한 회의록을 작성할 수 있습니다\"\n ],\n \"related_projects\": [\"회의록 시스템\", \"AI 자동화 프로젝트\"],\n \"past_discussions\": [\n {\"date\": \"2025-01-15\", \"meeting\": \"프로젝트 회의\", \"summary\": \"RAG 시스템 도입 결정\"},\n {\"date\": \"2025-01-10\", \"meeting\": \"기술 세미나\", \"summary\": \"RAG 아키텍처 소개\"}\n ],\n \"references\": [\"doc_12345\", \"doc_12346\", \"doc_12347\"]\n}" - } - ], - "model": "claude-3-5-sonnet-20241022", - "stop_reason": "end_turn", - "usage": { - "input_tokens": 1250, - "output_tokens": 320 - } -} -``` - ---- - -## 단계별 구현 로드맵 - -### Phase 1: 기본 기능 (2주) - -**목표**: 회의록 기반 최소 기능 구현 및 사용자 테스트 - -#### 구현 범위 -- [x] 회의록 DB 연동 -- [x] 용어 감지 엔진 (Trie 자료구조) -- [x] OpenAI Embedding API 연동 -- [x] Pinecone 벡터 검색 -- [x] Claude API 호출 (기본 프롬프트) -- [x] UI: 점선 밑줄 하이라이트, 바텀 시트 툴팁 - -#### 성능 목표 -- 응답 시간: **5초 이내** -- 용어 감지 정확도: **70% 이상** -- 관련 문서 정확도: **60% 이상** - -#### 제약 사항 -- 캐시 없음 (모든 요청 실시간 처리) -- 회의록만 검색 (위키, 문서, 이메일 미포함) - -#### 배포 전략 -- Beta 테스트: 20명 (개발팀 10명, 기획팀 5명, 경영지원팀 5명) -- 피드백 수집: Google Forms 설문 + 주간 인터뷰 - ---- - -### Phase 2: 성능 개선 (4주) - -**목표**: 캐시 도입 + 위키 추가 + 하이브리드 검색 - -#### 구현 범위 -- [x] Redis 캐시 레이어 - - TTL: 자주 쓰이는 용어 7일, 드문 용어 1일 - - Cache Warming: 상위 50개 용어 사전 캐싱 -- [x] 사내 위키 연동 (Confluence, Notion) -- [x] 하이브리드 검색 (벡터 + 키워드) - - 벡터 유사도: 70% 가중치 - - 키워드 매칭: 30% 가중치 -- [x] 프롬프트 최적화 (Few-shot learning) - -#### 성능 목표 -- 응답 시간: **3초 이내** (캐시 히트 시 0.5초) -- 용어 감지 정확도: **85% 이상** -- 관련 문서 정확도: **75% 이상** -- 캐시 히트율: **60% 이상** - -#### 배포 전략 -- Beta 테스트 확대: 50명 -- A/B 테스트: 캐시 vs 캐시 없음, 하이브리드 vs 벡터 단독 - ---- - -### Phase 3: 고도화 (6주) - -**목표**: 전체 데이터 통합 + 시맨틱 필터링 + 부서별 커스터마이징 - -#### 구현 범위 -- [x] 프로젝트 문서 연동 (Google Drive, SharePoint) -- [x] 이메일 연동 (Outlook, Gmail) -- [x] 시맨틱 필터링 - - 부서별 용어 우선순위 - - 프로젝트별 문맥 가중치 -- [x] 2단계 캐싱 - - L1: Redis (Hot data, TTL 7일) - - L2: CDN (Static explanations, TTL 30일) -- [x] 용어 추천 시스템 - - "이 용어를 사전에 추가할까요?" 제안 - -#### 성능 목표 -- 응답 시간: **2초 이내** (캐시 히트 시 0.3초) -- 용어 감지 정확도: **90% 이상** -- 관련 문서 정확도: **80% 이상** -- 캐시 히트율: **80% 이상** - -#### 배포 전략 -- 전사 배포 (200명+) -- 부서별 용어 사전 큐레이션 워크숍 - ---- - -## 성능 및 비용 최적화 - -### 1. 캐싱 전략 - -#### Redis 캐시 구조 -``` -Key: term:{term_name}:{meeting_id} -Value: JSON (TermExplanation) -TTL: - - 자주 쓰이는 용어 (요청 >10회/월): 7일 - - 드문 용어 (요청 <10회/월): 1일 -``` - -#### Cache Warming -```python -# 매일 새벽 2시 실행 -def cache_warming(): - # 상위 50개 빈도 높은 용어 - top_terms = get_top_terms(limit=50) - - for term in top_terms: - # 최근 회의 맥락으로 미리 캐싱 - recent_meetings = get_recent_meetings(limit=5) - for meeting in recent_meetings: - explanation = explain_term(term, meeting) - cache_key = f"term:{term}:{meeting.id}" - redis.setex(cache_key, ttl=7*24*3600, value=json.dumps(explanation)) -``` - -### 2. 하이브리드 검색 - -```python -def hybrid_search(query, top_k=5): - """ - 벡터 검색 + 키워드 검색 결합 - """ - # 1. 벡터 검색 (70% 가중치) - query_embedding = generate_embedding(query) - vector_results = pinecone_index.query( - vector=query_embedding, - top_k=top_k * 2, # 2배수 조회 - include_metadata=True - ) - - # 2. 키워드 검색 (30% 가중치) - keyword_results = elasticsearch.search( - index="meetings", - body={ - "query": { - "multi_match": { - "query": query, - "fields": ["title^3", "content", "tags^2"] - } - } - }, - size=top_k * 2 - ) - - # 3. 점수 결합 (Normalized) - combined_scores = {} - - for match in vector_results.matches: - combined_scores[match.id] = match.score * 0.7 - - for hit in keyword_results['hits']['hits']: - doc_id = hit['_id'] - if doc_id in combined_scores: - combined_scores[doc_id] += hit['_score'] * 0.3 - else: - combined_scores[doc_id] = hit['_score'] * 0.3 - - # 4. 상위 k개 반환 - sorted_results = sorted(combined_scores.items(), key=lambda x: x[1], reverse=True) - return sorted_results[:top_k] -``` - -### 3. Claude API 비용 절감 - -#### 비용 구조 -``` -Claude 3.5 Sonnet: -- Input: $3 / 1M tokens -- Output: $15 / 1M tokens - -월 예상 비용 (500 회의록, 평균 10회 용어 설명): -- 총 요청: 5,000회 -- 평균 Input: 1,500 tokens/요청 -- 평균 Output: 300 tokens/요청 - -비용 = (5000 * 1500 * $3 / 1M) + (5000 * 300 * $15 / 1M) - = $22.5 + $22.5 - = $45/월 - -캐시 적용 시 (60% 히트율): - = $45 * 0.4 = $18/월 -``` - -#### Rate Limiting -```typescript -import { RateLimiter } from 'limiter'; - -const limiter = new RateLimiter({ - tokensPerInterval: 60, // 분당 60회 - interval: 'minute' -}); - -async function callClaudeWithRateLimit(prompt: string) { - await limiter.removeTokens(1); - return await anthropic.messages.create({...}); -} -``` - ---- - -## 품질 검증 기준 - -### 1. 자동 검증 - -#### 용어 감지 정확도 -```python -def calculate_term_detection_accuracy(): - """ - 테스트 세트: 100개 회의록, 200개 용어 - """ - test_data = load_test_data("term_detection_test.json") - - correct = 0 - total = len(test_data) - - for sample in test_data: - detected_terms = detect_terms(sample.content) - expected_terms = sample.expected_terms - - # F1 Score 계산 - precision = len(set(detected_terms) & set(expected_terms)) / len(detected_terms) - recall = len(set(detected_terms) & set(expected_terms)) / len(expected_terms) - f1 = 2 * (precision * recall) / (precision + recall) - - correct += f1 - - return (correct / total) * 100 -``` - -#### 설명 품질 평가 -```python -def evaluate_explanation_quality(): - """ - 사람 평가 + 자동 평가 결합 - """ - test_cases = load_test_cases("explanation_quality.json") - - scores = [] - - for case in test_cases: - explanation = explain_term(case.term, case.context) - - # 자동 평가 (1-5점) - auto_score = 0 - - # 1) 정의 포함 여부 (1점) - if len(explanation.definition) > 10: - auto_score += 1 - - # 2) 맥락 설명 포함 여부 (1점) - if len(explanation.context) > 20: - auto_score += 1 - - # 3) 사용 사례 포함 여부 (1점) - if len(explanation.usage_examples) >= 1: - auto_score += 1 - - # 4) 관련 문서 연결 여부 (1점) - if len(explanation.references) >= 1: - auto_score += 1 - - # 5) 과거 논의 포함 여부 (1점) - if len(explanation.past_discussions) >= 1: - auto_score += 1 - - scores.append(auto_score) - - return sum(scores) / len(scores) -``` - -### 2. 사람 평가 - -#### 평가 기준 -| 항목 | 배점 | 기준 | -|------|------|------| -| **정확성** | 30점 | 용어 정의가 정확한가? | -| **맥락 적합성** | 30점 | 현재 회의 맥락과 관련성이 높은가? | -| **실용성** | 20점 | 실제 업무에 도움이 되는가? | -| **가독성** | 10점 | 이해하기 쉬운가? | -| **완성도** | 10점 | 관련 문서, 과거 논의 포함 여부 | - -#### 평가 프로세스 -1. 매 Sprint 종료 시 20개 샘플 평가 -2. 평가자: Beta 테스터 10명 (무작위 선정) -3. 목표 점수: 80점 이상 - ---- - -## 운영 및 모니터링 - -### 1. 성능 메트릭 - -#### Prometheus 메트릭 -```typescript -import { Counter, Histogram, Gauge } from 'prom-client'; - -// 요청 수 -const requestCounter = new Counter({ - name: 'rag_requests_total', - help: 'Total number of RAG requests', - labelNames: ['status', 'cache_hit'] -}); - -// 응답 시간 -const responseTimeHistogram = new Histogram({ - name: 'rag_response_time_seconds', - help: 'RAG response time in seconds', - buckets: [0.5, 1, 2, 3, 5] -}); - -// 캐시 히트율 -const cacheHitRate = new Gauge({ - name: 'rag_cache_hit_rate', - help: 'Cache hit rate percentage' -}); - -// Claude API 비용 -const apiCostCounter = new Counter({ - name: 'claude_api_cost_usd', - help: 'Estimated Claude API cost in USD' -}); -``` - -#### Grafana 대시보드 -``` -┌────────────────────────────────────────────┐ -│ RAG 시스템 성능 모니터링 │ -├────────────────────────────────────────────┤ -│ [ 실시간 요청 수 ] [ 평균 응답 시간 ] │ -│ 125 req/min 2.3s │ -├────────────────────────────────────────────┤ -│ [ 캐시 히트율 ] [ Claude API 비용 ] │ -│ 65% $18/월 │ -├────────────────────────────────────────────┤ -│ [ 용어 감지 정확도 ] [ 설명 품질 점수 ] │ -│ 88% 85/100 │ -└────────────────────────────────────────────┘ -``` - -### 2. 알림 설정 - -#### Alertmanager 규칙 -```yaml -groups: - - name: rag_alerts - rules: - - alert: HighResponseTime - expr: rag_response_time_seconds > 5 - for: 5m - labels: - severity: warning - annotations: - summary: "RAG 응답 시간 초과 (>5초)" - - - alert: LowCacheHitRate - expr: rag_cache_hit_rate < 50 - for: 10m - labels: - severity: info - annotations: - summary: "캐시 히트율 낮음 (<50%)" - - - alert: ClaudeAPIError - expr: increase(rag_requests_total{status="error"}[5m]) > 10 - labels: - severity: critical - annotations: - summary: "Claude API 오류 급증" -``` - -### 3. 로깅 전략 - -```typescript -import winston from 'winston'; - -const logger = winston.createLogger({ - level: 'info', - format: winston.format.json(), - transports: [ - new winston.transports.File({ filename: 'rag-error.log', level: 'error' }), - new winston.transports.File({ filename: 'rag-combined.log' }) - ] -}); - -// 로그 예시 -logger.info('Term explanation generated', { - term: 'RAG', - meeting_id: 'meeting_12345', - response_time_ms: 2300, - cache_hit: false, - related_docs_count: 5, - claude_tokens: { input: 1500, output: 320 } -}); -``` - ---- - -## 부록 - -### A. API 명세서 - -#### POST /api/rag/explain -**요청**: -```json -{ - "term": "RAG", - "meeting_id": "meeting_12345", - "context": { - "meeting_title": "주간 스크럼", - "date": "2025-01-20T14:00:00Z", - "participants": ["김민준", "박서연"], - "project_name": "회의록 시스템" - } -} -``` - -**응답**: -```json -{ - "term": "RAG", - "explanation": { - "definition": "Retrieval-Augmented Generation의 약자로...", - "context": "우리 팀은 회의록 시스템에 RAG를 적용하여...", - "usage_examples": ["..."], - "related_projects": ["..."], - "past_discussions": [...], - "references": ["doc_12345"] - }, - "metadata": { - "response_time_ms": 2300, - "cache_hit": false, - "related_docs_count": 5, - "confidence_score": 0.92 - } -} -``` - -### B. 배치 작업 스케줄 - -| 작업 | 주기 | 시간 | 설명 | -|------|------|------|------| -| 회의록 벡터화 | 실시간 | - | 회의 종료 후 즉시 | -| 위키 동기화 | 매일 | 새벽 2시 | Confluence API 호출 | -| 캐시 워밍 | 매일 | 새벽 3시 | 상위 50개 용어 캐싱 | -| 용어 사전 갱신 | 주간 | 일요일 새벽 1시 | 신규 용어 자동 추가 | -| 성능 리포트 생성 | 주간 | 월요일 오전 9시 | 주간 성능 분석 | - ---- - -## 변경 이력 - -| 버전 | 날짜 | 변경 내용 | 작성자 | -|------|------|-----------|--------| -| 1.0 | 2025-01-20 | 초기 구현방안 작성 | 박서연, 이준호, 홍길동 | -| 2.0 | 2025-01-20 | 하이브리드형 로드맵으로 최종 승인 | 전체 팀 | - ---- - -**문서 끝**