diff --git a/.gitignore b/.gitignore index b583fb3d..c155892e 100644 --- a/.gitignore +++ b/.gitignore @@ -31,7 +31,7 @@ yarn-error.log* .pnpm-debug.log* # env files (can opt-in for committing if needed) -.env +.env.local .env.local # vercel diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 00000000..98791984 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,10 @@ +# 디폴트 무시된 파일 +/shelf/ +/workspace.xml +# 쿼리 파일을 포함한 무시된 디폴트 폴더 +/queries/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml +# 에디터 기반 HTTP 클라이언트 요청 +/httpRequests/ diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml new file mode 100644 index 00000000..932f7d1b --- /dev/null +++ b/.idea/codeStyles/Project.xml @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml new file mode 100644 index 00000000..79ee123c --- /dev/null +++ b/.idea/codeStyles/codeStyleConfig.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 00000000..03d9549e --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 00000000..47478b91 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 00000000..0e293e75 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/prettier.xml b/.idea/prettier.xml new file mode 100644 index 00000000..b0c1c68f --- /dev/null +++ b/.idea/prettier.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/study-platform-client.iml b/.idea/study-platform-client.iml new file mode 100644 index 00000000..d6ebd480 --- /dev/null +++ b/.idea/study-platform-client.iml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 00000000..35eb1ddf --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 289592f6..8a8f4274 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,8 +6,8 @@ WORKDIR /app COPY . . -# 빌드시점에 .env 파일 복사 -COPY .env .env +# 빌드시점에 .env.local 파일 복사 +COPY .env.local .env RUN yarn install && yarn build @@ -22,7 +22,7 @@ COPY --from=builder /app/public ./public COPY --from=builder /app/package.json ./package.json COPY --from=builder /app/yarn.lock ./yarn.lock -# 런타임시점에도 .env 파일 복사 +# 런타임시점에도 .env.local 파일 복사 COPY --from=builder /app/.env .env # devDependencies는 설치하지 않고 dependencies만 설치 diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 84b453b0..9eb4a8f1 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -13,5 +13,5 @@ services: ports: - '3000:3000' env_file: - - .env + - .env.local restart: unless-stopped diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index 49097f14..92029bfd 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -13,5 +13,5 @@ services: ports: - '3000:3000' env_file: - - .env + - .env.local restart: unless-stopped diff --git a/docs/archive-refactor-actions-plan.md b/docs/archive-refactor-actions-plan.md new file mode 100644 index 00000000..d960806a --- /dev/null +++ b/docs/archive-refactor-actions-plan.md @@ -0,0 +1,40 @@ +# 아카이브 리팩토링 계획 (행동 단위 훅 도입) + +작성일: 2026-01-30 + +## 목표 + +- UI가 “행동 판단/순서/부수효과”를 직접 처리하지 않도록 분리 +- Admin처럼 **의미 있는 행동 단위 훅**으로 책임 이동 + +## 문제점 (현재) + +- UI에서 직접 API 훅 호출 + 순서 결정 + - 예: 링크 열기 → 조회수 기록 +- UI가 도메인 행동의 오케스트레이션까지 담당 + +## 계획 + +### 1) 행동 훅 추가 + +- `features/archive/model/use-archive-actions.ts` +- 제공 기능 + - `toggleLike(id)` + - `toggleBookmark(id)` + - `openAndRecordView(item)` (window.open + view 기록) + - (옵션) `hide(item)`는 TODO 자리 유지 + +### 2) UI 수정 + +- `archive-tab-client.tsx`에서 개별 mutation 훅 제거 +- 대신 `useArchiveActions`로 통합 + +### 3) 책임 정리 + +- UI는 이벤트 발생만 전달 +- 순서/부수효과/실패 처리 정책은 훅 내부 + +## 완료 기준 + +- UI에서 `useToggleArchiveLike/Bookmark/useRecordArchiveView` 직접 호출 제거 +- 행동 훅을 통한 단일 인터페이스 사용 diff --git a/docs/archive-refactor-plan.md b/docs/archive-refactor-plan.md new file mode 100644 index 00000000..f65aa06d --- /dev/null +++ b/docs/archive-refactor-plan.md @@ -0,0 +1,132 @@ +# 아카이브 리팩토링 계획 (Admin 구조 기준 반성 포인트 정리) + +작성일: 2026-01-30 + +## 배경 + +Admin 영역은 **서버/클라이언트 경계**, **API 분리**, **모델/타입/상수 정리**가 비교적 명확함. +반면 아카이브는 기능/데이터/상태가 한 파일에 뭉쳐 있고, 서버 프리패치와 클라이언트 상호작용의 경계도 흐림. + +이 문서는 **Admin 구조를 기준으로 아카이브에서 반성할 포인트**를 추리고, 구체적인 리팩토링 계획을 정의한다. + +--- + +## 1. Admin 구조에서 배워야 할 기준 (반성 포인트) + +### A. 서버/클라이언트 분리 원칙 + +- Admin은 `*.server.ts` API와 클라이언트 API가 분리되어 있음. +- 아카이브도 `get-archive.server.ts`가 있지만, **UI/모델에서 초기 데이터 주입 전략이 불명확**함. + +### B. 모델 레이어의 역할 분리 + +- Admin은 Query/Mutation 훅이 **목적별 파일**로 분리되어 있음. +- 아카이브는 목록/북마크/좋아요/조회수가 각각 흩어져 있고, **UI에서 직접 조합하는 책임이 과도**함. + +### C. 상수/타입 분리 + +- Admin은 `const/member.ts`로 옵션/매핑을 분리. +- 아카이브는 옵션/필터 값, 정렬 타입, UI 상태 등이 **컴포넌트 내부에 고정**됨. + +### D. 컴포넌트 분해 수준 + +- Admin은 테이블/모달/필터 UI가 **작게 분해**되어 있음. +- 아카이브는 `archive-tab-client.tsx` 단일 파일에 UI/상태/이벤트/데이터 로직이 함께 있음. + +--- + +## 2. 현재 아카이브 문제점 (구체) + +### 1) UI + 데이터 + 상태가 단일 파일에 집중 + +- `src/features/archive/ui/archive-tab-client.tsx`가 + - 필터/검색/정렬 상태 + - API 훅 호출 + - 테이블/카드 뷰 렌더 + - 이벤트 처리 로직 + 모두 포함하고 있음. + +### 2) 서버 프리패치가 구조적으로 반영되지 않음 + +- 서버 래퍼에서 initialData를 주입은 했지만 + - 조건 분기 로직이 컴포넌트 내부에 단편적으로 흩어져 있음 + - 향후 param 증가 시 유지보수 어려움 + +### 3) 도메인 규칙이 하드코딩됨 + +- 정렬 옵션, 페이지 크기, UI 문구가 직접 박혀 있어 + - 변경 시 여러 위치 수정 필요 + - 테스트/재사용 어려움 + +--- + +## 3. 리팩토링 목표 + +1. **Admin 수준의 구조 분리 달성** +2. **서버-클라이언트 경계 명확화** +3. **도메인 규칙(옵션/상수) 분리** +4. **UI 컴포넌트 재사용 가능 단위로 분해** + +--- + +## 4. 리팩토링 상세 계획 + +### Step 1) UI 구조 분리 + +- `archive-tab-client.tsx`를 아래 구성으로 분해 + +``` +features/archive/ui/ + archive-tab-client.tsx // 컨테이너 (상태/데이터/조합) + archive-header.tsx // 상단 타이틀/관리자 토글 + archive-filters.tsx // 필터/검색/정렬 UI + archive-grid.tsx // 카드 뷰 + archive-list.tsx // 리스트 뷰 +``` + +### Step 2) 상수/옵션 분리 + +- `features/archive/const/archive.ts` 신설 + - 정렬 옵션 리스트 + - 기본 page size + - UI 텍스트 상수 + +### Step 3) 모델 레이어 정리 + +- `use-archive-query.ts`는 유지하되 + - 파라미터 생성 로직을 **hooks/util로 분리** +- Mutations도 목적별로 **use-xxx-mutation.ts**에 명확한 책임 유지 + +### Step 4) 서버 프리패치 표준화 + +- 서버 래퍼에서 `initialParams` 계산/보존 +- 클라이언트는 `params` 변경 시에만 재조회하도록 + - 비교 함수를 util로 분리 + +### Step 5) 타입베이스드 정리 + +- 현재 `src/types/archive.ts`가 존재하므로 + - Archive 전용 타입은 여기로 유지 + - API layer에서는 `ArchiveResponse`만 사용 + +--- + +## 5. 체크리스트 + +- [ ] `archive-tab-client.tsx` 분해 완료 +- [ ] `features/archive/const/archive.ts` 추가 +- [ ] 서버 초기 데이터 주입 흐름 문서화 +- [ ] UI/데이터 로직 분리된 상태 확인 + +--- + +## 6. 결과 기대 효과 + +- 변경 범위가 줄어듦 (UI/데이터 분리) +- 유지보수 용이 +- 서버 프리패치 일관성 확보 +- Admin과 유사한 코드 품질 체계 확보 + +--- + +필요하면 위 계획에 맞춰 바로 리팩토링 진행 가능. diff --git a/docs/backend-api-change-request-study-history.md b/docs/backend-api-change-request-study-history.md new file mode 100644 index 00000000..da425001 --- /dev/null +++ b/docs/backend-api-change-request-study-history.md @@ -0,0 +1,212 @@ +# 백엔드 API 명세 변경 요청서 + +## 요청 일자 + +2024년 (작성 시점 기준) + +## 요청 항목 + +1대1 스터디 기록 조회 API (`GET /api/v1/study/daily/history`) 관련 명세 확인 및 변경 요청 + +--- + +## 1. 변경 배경 + +프론트엔드에서 "나의 1대1 스터디 기록" 페이지의 UI 개선을 위해 다음 변경이 필요합니다: + +- 테이블 컬럼명 변경: "출석" → "역할수행여부" +- 테이블에 "진행상태" 컬럼 추가 + +--- + +## 2. 현재 API 사용 현황 + +### 2.1 API 엔드포인트 + +``` +GET /api/v1/study/daily/history +``` + +### 2.2 요청 파라미터 + +```typescript +{ + page?: number; // 페이지 번호 (0부터 시작) + size?: number; // 페이지 크기 + startDate?: string; // 시작 날짜 (YYYY-MM-DD) + endDate?: string; // 종료 날짜 (YYYY-MM-DD) + sort?: string; // 정렬 기준 (예: "createdAt,desc") +} +``` + +### 2.3 현재 응답 구조 (프론트엔드 기대값) + +```typescript +{ + statusCode: number; + timestamp: string; + content: { + content: StudyHistoryContent[]; + totalElements: number; + totalPages: number; + // ... 기타 페이지네이션 정보 + }; + message: string; +} + +interface StudyHistoryContent { + studyId: number; + title: string; + scheduledAt: string; // ISO Date String + status: 'COMPLETE' | 'IN_PROGRESS' | 'PENDING'; // 스터디 진행 상태 + studyLink: string | null; + participation: { + role: 'INTERVIEWER' | 'INTERVIEWEE'; // 내 역할 + attendance: 'PRESENT' | 'PENDING' | 'ABSENT'; // 역할 수행 여부 + }; + partner: { + memberId: number; + nickname: string; + profileImageUrl: string | null; + }; +} +``` + +--- + +## 3. 요청 사항 + +### 3.1 필드명 및 값 정의 확인 요청 + +현재 프론트엔드에서 사용 중인 필드들의 명확한 정의를 확인하고 싶습니다: + +#### 3.1.1 `participation.attendance` 필드 + +- **현재 사용 값**: `PRESENT`, `PENDING`, `ABSENT` +- **요청 사항**: + - 각 값의 정확한 의미 확인 + - `PRESENT`: 역할을 수행했는지 여부를 나타내는가? + - `PENDING`: 아직 진행 전인 상태인가? + - `ABSENT`: 불참 상태인가? +- **프론트엔드 표시**: + - `PRESENT` → "역할수행" (성공 아이콘) + - `PENDING` 또는 `ABSENT` → "미진행" (경고 아이콘) + +#### 3.1.2 `status` 필드 + +- **현재 사용 값**: `COMPLETE`, `IN_PROGRESS`, `PENDING` +- **요청 사항**: + - 각 값의 정확한 의미 확인 + - `COMPLETE`: 스터디가 완전히 종료된 상태인가? + - `IN_PROGRESS`: 스터디가 진행 중인 상태인가? + - `PENDING`: 스터디가 아직 시작되지 않은 상태인가? +- **프론트엔드 표시**: + - `COMPLETE` → "완료" (성공 아이콘) + - `IN_PROGRESS` → "진행중" (로딩 아이콘) + - `PENDING` → "대기중" (시계 아이콘) - 필요시 추가 + +### 3.2 API 명세 문서화 요청 + +현재 `GET /api/v1/study/daily/history` API가 OpenAPI 스펙에 포함되어 있지 않은 것으로 보입니다. +다음 내용을 포함한 명세 문서화를 요청합니다: + +1. **엔드포인트 정보** + - 경로: `/api/v1/study/daily/history` + - 메서드: `GET` + - 인증: Bearer Token 필요 + +2. **요청 파라미터 상세** + - 각 파라미터의 타입, 필수 여부, 설명 + - 기본값 (있는 경우) + +3. **응답 스키마 상세** + - 각 필드의 타입 및 설명 + - Enum 값들의 의미 + - 예시 응답 + +4. **에러 응답** + - 가능한 에러 코드 및 메시지 + +--- + +## 4. 프론트엔드 변경 사항 + +### 4.1 UI 변경 + +- ✅ 테이블 헤더: "출석" → "역할수행여부" +- ✅ 테이블 행: "출석" 텍스트 → "역할수행" 텍스트 +- ✅ 테이블에 "진행상태" 컬럼 추가 + +### 4.2 데이터 매핑 + +```typescript +// API 응답 → UI 표시 +attendance: 'PRESENT' → "역할수행" (성공 아이콘) +attendance: 'PENDING' | 'ABSENT' → "미진행" (경고 아이콘) + +status: 'COMPLETE' → "완료" (성공 아이콘) +status: 'IN_PROGRESS' → "진행중" (로딩 아이콘) +status: 'PENDING' → "대기중" (시계 아이콘) - 필요시 +``` + +--- + +## 5. 확인 필요 사항 + +1. **`attendance`와 `status`의 차이점** + - `attendance`: 사용자가 자신의 역할을 수행했는지 여부 + - `status`: 스터디 전체의 진행 상태 + - 이 이해가 맞는지 확인 필요 + +2. **값의 일관성** + - `attendance`의 `PENDING`과 `status`의 `PENDING`이 같은 의미인지? + - 각 필드가 언제 어떤 값으로 설정되는지? + +3. **추가 필드 필요 여부** + - 현재 응답 구조로 UI 요구사항을 충족할 수 있는지 확인 + - 추가로 필요한 필드가 있는지 검토 + +--- + +## 6. 예상 일정 + +- 프론트엔드 UI 변경: ✅ 완료 +- 백엔드 API 명세 확인 및 문서화: 백엔드 팀 협의 필요 +- 통합 테스트: API 명세 확인 후 진행 + +--- + +## 7. 문의 사항 + +백엔드 팀에 다음 사항을 확인 요청드립니다: + +1. `GET /api/v1/study/daily/history` API의 정확한 명세서 제공 +2. `attendance`와 `status` 필드의 값 및 의미 명확화 +3. OpenAPI 스펙에 해당 API 추가 여부 +4. 추가로 필요한 필드나 변경 사항이 있는지 확인 + +--- + +## 8. 참고 자료 + +### 프론트엔드 파일 위치 + +- 컴포넌트: `src/components/study-history/study-history-row.tsx` +- 탭 컴포넌트: `src/components/home/tabs/study-history-tab.tsx` +- 타입 정의: `src/types/study-history.ts` +- API 호출: `src/features/study/history/api/get-my-study-history.ts` + +### 현재 사용 중인 타입 정의 + +```typescript +// src/types/study-history.ts +export type StudyRole = 'INTERVIEWER' | 'INTERVIEWEE'; +export type AttendanceStatus = 'PRESENT' | 'PENDING' | 'ABSENT'; +export type StudyStatus = 'COMPLETE' | 'IN_PROGRESS' | 'PENDING'; +``` + +--- + +## 작성자 + +프론트엔드 개발팀 diff --git a/docs/server-first-home-tabs-plan.md b/docs/server-first-home-tabs-plan.md new file mode 100644 index 00000000..6a97518b --- /dev/null +++ b/docs/server-first-home-tabs-plan.md @@ -0,0 +1,85 @@ +# 서버 우선 렌더링 전환 계획 (홈 탭: 밸런스게임/나의스터디기록/명예의전당/아카이브) + +작성일: 2026-01-30 + +## 목표 + +- **데이터 fetch·계산·조합은 서버에서 완료** +- 브라우저에는 **UI 껍데기 + 인터랙션만** 전달 +- App Router의 서버 경계를 명확히 유지 (`page.tsx`는 서버 컴포넌트) + +## 현재 문제 요약 + +- `app` 영역에서 `use client`가 상위에 위치해 **서버 컴포넌트 이점 상실** +- 홈 탭(밸런스게임/나의스터디기록/명예의전당/아카이브)의 데이터 fetch가 **클라이언트 훅 중심** +- 결과적으로 SSR/SEO/초기 로딩 이점이 약화 + +## 변경 방향 + +1. **`app` 영역은 서버 컴포넌트 유지** +2. **클라이언트 컴포넌트는 UI/인터랙션 전용** +3. **서버에서 초기 데이터 fetch 후 props로 전달** +4. 클라이언트는 **상호작용(필터, 무한스크롤)만 담당** + +## 단계별 작업 계획 + +### 1) 홈 탭 렌더 경계 정리 (완료) + +- `src/app/(service)/home/page.tsx`에서 `searchParams`로 activeTab 결정 +- `src/app/(service)/home/home-content.tsx`를 서버 컴포넌트로 변경 +- 클라이언트 로직은 `TabNavigation` 등 UI로 제한 + +### 2) 밸런스게임(community) 서버 프리패치 구조로 전환 + +- **서버 컴포넌트**: 초기 목록 fetch +- **클라이언트 컴포넌트**: 필터/무한스크롤/모달/작성 동작 담당 +- 작업 항목 + - 서버용 API 함수 분리 (`features/balance-game/api/*.server.ts`) + - `BalanceGamePage`에 `initialData` props 적용 + - `useBalanceGameListQuery`에 `initialData` 주입 + +### 3) 나의 스터디 기록(server first) + +- 서버에서 조회 후 리스트/캘린더 초기 데이터 렌더 +- 클라이언트는 viewMode/페이지 변경만 처리 +- 작업 항목 + - `features/study/history/api/*.server.ts` + - `StudyHistoryTab`를 서버 wrapper + client view로 분리 + +### 4) 명예의 전당(server first) + +- 서버에서 랭킹 데이터 fetch +- 클라이언트는 탭/필터 전환만 담당 +- 작업 항목 + - `features/hall-of-fame/api/*.server.ts` + - `HallOfFameTab` 분리 (Server wrapper + Client view) + +### 5) 아카이브(server first) + +- 서버에서 초기 리스트 fetch +- 클라이언트는 정렬/검색/북마크/좋아요 인터랙션 담당 +- 작업 항목 + - `features/archive/api/*.server.ts` + - `ArchiveTab` 분리 (Server wrapper + Client view) + +## 완료 기준 + +- `app` 내부 페이지에 `use client` 없음 +- 초기 렌더에 필요한 데이터는 서버에서 준비 +- 클라이언트 fetch는 **추가 상호작용에 한정** + +## 체크리스트 + +- [ ] `rg "use client" src/app` 에서 해당 섹션 page.tsx 제거 확인 +- [ ] 홈 탭 첫 진입 시 SSR 데이터 렌더 확인 +- [ ] 탭 전환/필터/무한스크롤 정상 동작 +- [ ] 네트워크 탭에서 초기 fetch가 서버에서 수행되는지 확인 + +## 참고 파일(현재 구조) + +- `src/app/(service)/home/page.tsx` +- `src/app/(service)/home/home-content.tsx` +- `src/components/home/tabs/community-tab.tsx` +- `src/components/home/tabs/study-history-tab.tsx` +- `src/components/home/tabs/hall-of-fame-tab.tsx` +- `src/components/home/tabs/archive-tab.tsx` diff --git a/docs/type-based-migration-plan-home-features.md b/docs/type-based-migration-plan-home-features.md new file mode 100644 index 00000000..6cae839d --- /dev/null +++ b/docs/type-based-migration-plan-home-features.md @@ -0,0 +1,116 @@ +# Type-based 정리 계획 (밸런스게임/나의스터디기록/명예의전당/아카이브) + +작성일: 2026-01-30 + +## 0. 현재 코드 상태 요약 (다른 팀원 코드 패턴) + +- 타입베이스드로 이미 쓰는 패턴 + - `src/components/home/tabs/archive-tab.tsx` → `src/types/archive` + - `src/components/home/tabs/study-history-tab.tsx` → `src/types/study-history` +- 아직 feature 타입을 쓰는 패턴 + - `src/components/home/tabs/community-tab.tsx` → `src/features/balance-game/types` + - `src/components/home/tabs/hall-of-fame-tab.tsx` → `src/features/hall-of-fame/types` + +즉, **팀 코드도 현재 혼재 상태**이며, 타입베이스드 기준으로는 Balance Game + Hall of Fame 쪽이 정리 대상임. + +## 1. 목표 + +- 홈 탭 관련 주요 도메인에서 **타입 정의를 `src/types`로 통일** +- 동일 의미 타입의 중복 정의 제거 +- import 경로 일관화 + +## 2. 범위 + +- 밸런스게임 (`home?tab=community`, `insights/weekly` 포함) +- 나의 스터디 기록 (`home?tab=history`) +- 명예의 전당 (`home?tab=hall-of-fame`) +- 아카이브 (`home?tab=archive`) + +## 3. 작업 원칙 + +- 타입은 기능 폴더가 아닌 **프로젝트 공용 타입으로 취급** +- UI/Model/API 어디서든 동일 타입 재사용 +- 타입명 충돌 시 **의미 중심 명명**으로 통일 + +## 4. 세부 작업 계획 + +### A. Balance Game 타입 정리 + +현재: + +- `src/features/balance-game/types.ts` +- `src/types/voting.ts` (투표 관련 타입 존재) + +선택지 (둘 중 하나 결정 필요): + +1. `src/types/balance-game.ts` 신설 및 이동 + - 기존 `src/features/balance-game/types.ts` 내용을 복사/이동 + - 기존 `src/types/voting.ts`는 유지 (단, 의미 분리) +2. `src/types/voting.ts`로 통합 + - Balance Game 타입을 voting 기준으로 통합 + - 중복/이름 불일치 정리 필요 + +**추천:** 1)로 분리하여 명확히 구분 (BalanceGame vs Voting 개념 혼재 방지) + +작업 항목: + +- `src/features/balance-game/types.ts` → `src/types/balance-game.ts` +- 아래 사용처 import 변경 + - `src/components/home/tabs/community-tab.tsx` + - `src/app/(service)/insights/weekly/page.tsx` + - `src/components/card/voting-card.tsx` + - `src/components/voting/voting-edit-modal.tsx` + - `src/components/voting/voting-detail-view.tsx` + - `src/components/discussion/comment-list.tsx` (BalanceGameComment 사용 시) +- `src/features/balance-game/api/balance-game-api.ts` 내부 타입 import도 변경 +- 기존 feature 타입 파일 삭제 + +### B. Hall of Fame 타입 정리 + +현재: + +- `src/features/hall-of-fame/types.ts` + +작업 항목: + +- `src/features/hall-of-fame/types.ts` → `src/types/hall-of-fame.ts` +- 사용처 import 변경 + - `src/components/home/tabs/hall-of-fame-tab.tsx` + - `src/features/hall-of-fame/api/hall-of-fame-api.ts` + - `src/features/hall-of-fame/model/use-hall-of-fame-query.ts` +- 기존 feature 타입 파일 삭제 + +### C. Study History / Archive 확인 + +- 이미 `src/types` 사용 중 +- 변경 필요 없음 +- 단, 신규 타입이 생길 때는 `src/types`로 합류하는지 체크 + +## 5. 검증 체크리스트 + +- `rg "features/(balance-game|hall-of-fame)/types" src` 결과 0건 +- `rg "@/types/(balance-game|hall-of-fame)" src` 사용처 정상 +- 빌드/타입체크 통과 +- 홈 탭 화면 동작 확인 + - community (밸런스게임 생성/리스트) + - history (기록 리스트/캘린더) + - hall-of-fame (랭킹/팀) + - archive (리스트/북마크/좋아요) + +## 6. 위험 요소 / 주의사항 + +- BalanceGame vs Voting 네이밍 혼재 + - 이미 `src/types/voting.ts`가 있음 → 통합 여부 결정 필요 +- 타입 중복 정의로 인해 API 응답 형태가 달라 보일 수 있음 + - `ApiResponse` 같은 래퍼 타입은 한 곳에서만 관리 권장 + +## 7. 예상 작업 순서 + +1. Balance Game 타입 이동 + import 정리 +2. Hall of Fame 타입 이동 + import 정리 +3. `rg`로 feature 타입 잔존 여부 확인 +4. 기본 화면 동작 수동 확인 + +--- + +필요하면 위 계획을 기준으로 바로 작업 진행 가능합니다. diff --git a/package.json b/package.json index c6c9358f..234db76c 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,7 @@ "date-fns": "^4.1.0", "dayjs": "^1.11.18", "embla-carousel-react": "^8.6.0", + "framer-motion": "^12.27.1", "googleapis": "^164.1.0", "lucide-react": "^0.475.0", "next": "15.2.8", @@ -106,8 +107,7 @@ "typescript-eslint": "^8.24.0", "vitest": "^3.1.1" }, - "resolutions": { + "resolutions": { "strip-ansi": "6.0.1" } - } diff --git a/src/app/(service)/home/home-content.tsx b/src/app/(service)/home/home-content.tsx new file mode 100644 index 00000000..b20f2849 --- /dev/null +++ b/src/app/(service)/home/home-content.tsx @@ -0,0 +1,45 @@ +import { Suspense } from 'react'; +import TabNavigation from '@/components/home/tab-navigation'; +import ArchiveTab from '@/features/study/one-to-one/archive/ui/archive-tab'; +import CommunityTab from '@/features/study/one-to-one/balance-game/ui/community-tab'; +import HallOfFameTab from '@/features/study/one-to-one/hall-of-fame/ui/hall-of-fame-tab'; +import StudyHistoryTab from '@/features/study/one-to-one/history/ui/study-history-tab'; +import StudyTab from '@/features/study/one-to-one/schedule/ui/home-study-tab'; + +interface HomeContentProps { + activeTab: string; +} + +export default function HomeContent({ activeTab }: HomeContentProps) { + const renderTabContent = () => { + switch (activeTab) { + case 'study': + return ; + case 'history': + return ; + case 'ranking': + return ; + case 'archive': + return ; + case 'community': + return ; + default: + return ; + } + }; + + return ( + <> + + + 로딩 중... + + } + > + {renderTabContent()} + + + ); +} diff --git a/src/app/(service)/home/page.tsx b/src/app/(service)/home/page.tsx index 0e2b8815..e9164fc8 100644 --- a/src/app/(service)/home/page.tsx +++ b/src/app/(service)/home/page.tsx @@ -1,8 +1,7 @@ import { Metadata } from 'next'; -import StudyCard from '@/features/study/schedule/ui/study-card'; import { generateMetadata as generateSEOMetadata } from '@/utils/seo'; -import Banner from '@/widgets/home/banner'; import Sidebar from '@/widgets/home/sidebar'; +import HomeContent from './home-content'; export const metadata: Metadata = generateSEOMetadata({ title: '홈 - ZERO-ONE', @@ -13,15 +12,22 @@ export const metadata: Metadata = generateSEOMetadata({ canonicalUrl: 'https://www.zeroone.it.kr/home', }); -export default async function Home() { +export default async function Home({ + searchParams, +}: { + searchParams?: Promise<{ tab?: string }>; +}) { + const resolvedSearchParams = await searchParams; + const activeTab = resolvedSearchParams?.tab || 'study'; + return (
- - + {/* 기존 기능을 100% 보존하면서 새로운 탭 시스템 추가 */} +
-
diff --git a/src/app/(service)/insights/page.tsx b/src/app/(service)/insights/page.tsx index c725e109..77399877 100644 --- a/src/app/(service)/insights/page.tsx +++ b/src/app/(service)/insights/page.tsx @@ -92,6 +92,17 @@ export default async function BlogPage({ searchParams }: BlogPageProps) { {category.name} ))} + + + 위클리 + + NEW + + + {/* 아티클 목록 */} diff --git a/src/app/(service)/insights/weekly/[id]/page.tsx b/src/app/(service)/insights/weekly/[id]/page.tsx new file mode 100644 index 00000000..24c45459 --- /dev/null +++ b/src/app/(service)/insights/weekly/[id]/page.tsx @@ -0,0 +1,12 @@ +import VotingDetailPageClient from '@/features/study/one-to-one/balance-game/ui/voting-detail-page-client'; + +export default async function VotingDetailPage({ + params, +}: { + params: Promise<{ id: string }>; +}) { + const { id } = await params; + const votingId = Number(id); + + return ; +} diff --git a/src/app/(service)/insights/weekly/page.tsx b/src/app/(service)/insights/weekly/page.tsx new file mode 100644 index 00000000..0b380169 --- /dev/null +++ b/src/app/(service)/insights/weekly/page.tsx @@ -0,0 +1,5 @@ +import BalanceGamePage from '@/features/study/one-to-one/balance-game/ui/balance-game-page'; + +export default function VotingPage() { + return ; +} diff --git a/src/app/(service)/layout.tsx b/src/app/(service)/layout.tsx index 8954c45e..41b1a551 100644 --- a/src/app/(service)/layout.tsx +++ b/src/app/(service)/layout.tsx @@ -42,9 +42,9 @@ export default function ServiceLayout({ -
+
-
{children}
+
{children}
diff --git a/src/app/(service)/one-on-one/page.tsx b/src/app/(service)/one-on-one/page.tsx new file mode 100644 index 00000000..8facbe95 --- /dev/null +++ b/src/app/(service)/one-on-one/page.tsx @@ -0,0 +1,5 @@ +import OneOnOnePage from '@/features/study/one-to-one/ui/one-on-one-page'; + +export default function OneOnOnePageRoute() { + return ; +} diff --git a/src/components/card/discussion-card.tsx b/src/components/card/discussion-card.tsx new file mode 100644 index 00000000..b16283b1 --- /dev/null +++ b/src/components/card/discussion-card.tsx @@ -0,0 +1,135 @@ +import { formatDistanceToNow } from 'date-fns'; +import { ko } from 'date-fns/locale'; +import { MessageCircle, ThumbsUp, ThumbsDown, Eye, Clock } from 'lucide-react'; +import React from 'react'; +import { cn } from '@/components/ui/(shadcn)/lib/utils'; +import UserAvatar from '@/components/ui/avatar'; +import UserProfileModal from '@/entities/user/ui/user-profile-modal'; +import { TOPIC_LABELS } from '@/mocks/discussion-mock-data'; +import { Discussion } from '@/types/discussion'; + +interface DiscussionCardProps { + discussion: Discussion; + onClick?: () => void; +} + +export default function DiscussionCard({ + discussion, + onClick, +}: DiscussionCardProps) { + const timeAgo = formatDistanceToNow(new Date(discussion.lastActivityAt), { + addSuffix: true, + locale: ko, + }); + + return ( +
+ {/* 헤더: 작성자 정보 & 주제 */} +
+
+ {/* 아바타 & 닉네임 */} +
e.stopPropagation()}> + +
+ +
+ + {discussion.author.nickname} + +
+ } + /> +
+ + {/* 시간 */} +
+ +
+ + {timeAgo} +
+
+
+ + {/* 주제 배지 */} +
+ {TOPIC_LABELS[discussion.topic]} +
+
+ + {/* 제목 */} +

+ {discussion.title} +

+ + {/* 요약 */} +

+ {discussion.summary} +

+ + {/* 태그 */} + {discussion.tags.length > 0 && ( +
+ {discussion.tags.map((tag) => ( + + #{tag} + + ))} +
+ )} + + {/* 하단 메타 정보 */} +
+
+ {/* 찬성 */} +
+ + {discussion.vote.agreeCount} +
+ + {/* 반대 */} +
+ + {discussion.vote.disagreeCount} +
+ + {/* 댓글 */} +
+ + {discussion.commentCount} +
+ + {/* 조회수 */} +
+ + {discussion.viewCount.toLocaleString()} +
+
+
+
+ ); +} diff --git a/src/components/card/voting-card.tsx b/src/components/card/voting-card.tsx new file mode 100644 index 00000000..81b2b836 --- /dev/null +++ b/src/components/card/voting-card.tsx @@ -0,0 +1,133 @@ +import { MessageCircle, Users } from 'lucide-react'; +import Link from 'next/link'; +import React from 'react'; +import { cn } from '@/components/ui/(shadcn)/lib/utils'; +import UserAvatar from '@/components/ui/avatar'; +import UserProfileModal from '@/entities/user/ui/user-profile-modal'; +import { BalanceGame } from '@/types/balance-game'; +import VoteTimer from '../voting/vote-timer'; + +interface VotingCardProps { + voting: BalanceGame; + onClick?: () => void; +} + +export default function VotingCard({ voting, onClick }: VotingCardProps) { + const topOption = voting.options.reduce((prev, current) => + prev.percentage > current.percentage ? prev : current, + ); + + // myVote can be null or number (optionId) + const hasVoted = voting.myVote !== undefined && voting.myVote !== null; + + const cardContent = ( +
{ + e.preventDefault(); + onClick(); + } + : undefined + } + > + {/* 헤더: 작성자 & 상태 */} +
+ {/* 작성자 정보 */} +
e.stopPropagation()}> + +
+ +
+ + {voting.author.nickname} + +
+ } + /> +
+ + {/* 타이머 표시 */} + +
+ + {/* 제목 */} +

+ {voting.title} +

+ + {/* 태그 - 제목 바로 아래에 표시 */} + {voting.tags && Array.isArray(voting.tags) && voting.tags.length > 0 && ( +
+ {voting.tags.map((tag, index) => ( + + #{tag} + + ))} +
+ )} + + {/* 설명 (있으면) */} + {voting.description && ( +

+ {voting.description} +

+ )} + + {/* 간단한 투표 결과 미리보기 (투표했을 때만) */} + {hasVoted && ( +
+
+ 현재 1위 +
+
+ + {topOption.label} + + + {topOption.percentage.toFixed(1)}% + +
+
+ )} + + {/* 하단 메타 정보 */} +
+ {/* 총 투표 수 */} +
+ + {voting.totalVotes.toLocaleString()} +
+ + {/* 댓글 수 */} +
+ + {voting.commentCount || 0} +
+
+ + ); + + // onClick이 있으면 Link 없이 렌더링, 없으면 Link로 감싸기 + if (onClick) { + return cardContent; + } + + return {cardContent}; +} diff --git a/src/components/discussion/comment-form.tsx b/src/components/discussion/comment-form.tsx new file mode 100644 index 00000000..df2f9271 --- /dev/null +++ b/src/components/discussion/comment-form.tsx @@ -0,0 +1,104 @@ +import { zodResolver } from '@hookform/resolvers/zod'; +import { Send, Loader2 } from 'lucide-react'; +import React from 'react'; +import { useForm } from 'react-hook-form'; +import { cn } from '@/components/ui/(shadcn)/lib/utils'; +import { CommentFormSchema, CommentFormData } from '@/types/schemas/zod-schema'; + +interface CommentFormProps { + onSubmit: (data: CommentFormData) => void | Promise; + isSubmitting?: boolean; + placeholder?: string; + autoFocus?: boolean; + initialValue?: string; + onCancel?: () => void; +} + +export default function CommentForm({ + onSubmit, + isSubmitting = false, + placeholder = '댓글을 입력하세요...', + autoFocus = false, + initialValue = '', + onCancel, +}: CommentFormProps) { + const { + register, + handleSubmit, + formState: { errors }, + reset, + } = useForm({ + resolver: zodResolver(CommentFormSchema), + defaultValues: { + content: initialValue, + }, + }); + + const handleFormSubmit = async (data: CommentFormData) => { + await onSubmit(data); + reset(); + }; + + return ( +
+
+
+