Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
f64be61
feat: 약속 완료 시 약속 상세 페이지 UI 변경(#87)
haruyam15 Feb 19, 2026
38dd474
feat: 약속 회고 상세 페이지 추가 (#87)
haruyam15 Feb 23, 2026
5641f2e
feat:아코디언 컴포넌트 추가(#87)
haruyam15 Feb 23, 2026
a7cef8e
design: 아코디언 컴포넌트css 수정(#87)
haruyam15 Feb 23, 2026
66cc74e
feat: 수집된 사전의견 조회 api 개발(#87)
haruyam15 Feb 23, 2026
276c327
feat: Dropzone 공통컴포넌트 생성 및 파일 업로드 기능구현(#87)
haruyam15 Feb 23, 2026
eae325a
Merge branch 'develop' into feat/meeting-retro-87
haruyam15 Feb 24, 2026
57300d3
design: AI요약 버튼 디자인 수정(#87)
haruyam15 Feb 24, 2026
540520b
style: 프리티어(#87)
haruyam15 Feb 24, 2026
42678b0
refactor: 파일 업로드 검증 강화 및 의존성 정리 (#87)
haruyam15 Feb 24, 2026
4207c8b
refactor: 에러처리 개선 (#87)
haruyam15 Feb 24, 2026
b2d2efb
design: 아코디언css 수정 (#87)
haruyam15 Feb 24, 2026
e70adaf
refactor: GetCollectedAnswersParams 배럴 추가(#87)
haruyam15 Feb 24, 2026
8326316
style: 주석제거(#87)
haruyam15 Feb 25, 2026
d1d4dbb
refactor: AlertIcon 색 커스텀 기능 추가(#87)
haruyam15 Feb 25, 2026
ffa1dad
refactor: 회고 아이콘을 SVG 파일로 전환 (#87)
haruyam15 Feb 25, 2026
306142a
refactor: AlertIcon적용 (#87)
haruyam15 Feb 25, 2026
44ba5d3
refactor: AlertIcon size prop추가(#87)
haruyam15 Feb 25, 2026
740bcb0
refactor: 주제 목록 에러 처리 개선 (#87)
haruyam15 Feb 25, 2026
4823729
fix: 버튼 프로퍼티수정(#87)
haruyam15 Feb 25, 2026
97f8084
feat: 회고 상태 기반 라우트 분기 처리 추가 (#87)
haruyam15 Feb 25, 2026
211d405
feat: 목데이터에 회고상태 추가(#87)
haruyam15 Feb 25, 2026
38861a1
refactor: params검증로직 페이지에서 처리(#87)
haruyam15 Feb 25, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
"vite": "^7.2.4"
},
"dependencies": {
"@radix-ui/react-accordion": "^1.2.3",
"@radix-ui/react-avatar": "^1.1.11",
"@radix-ui/react-checkbox": "^1.3.3",
"@radix-ui/react-dialog": "^1.1.15",
Expand Down
68 changes: 68 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 2 additions & 11 deletions src/features/meetings/hooks/useMeetingDetail.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,20 +23,11 @@ import { meetingQueryKeys } from './meetingQueryKeys'
*
*/
export const useMeetingDetail = (meetingId: number) => {
const isValidMeetingId = !Number.isNaN(meetingId) && meetingId > 0

// 유효하지 않은 meetingId는 detail 키 대신 details 키 사용
// NaN이 null로 직렬화되어 캐시 충돌하는 것을 방지
const queryKey = isValidMeetingId
? meetingQueryKeys.detail(meetingId)
: meetingQueryKeys.details()

return useQuery<GetMeetingDetailResponse, ApiError>({
// eslint-disable-next-line @tanstack/query/exhaustive-deps
queryKey,
queryKey: meetingQueryKeys.detail(meetingId),
queryFn: () => getMeetingDetail(meetingId),
// meetingId가 유효할 때만 쿼리 실행
enabled: isValidMeetingId,
enabled: meetingId > 0,
// 캐시 데이터 10분간 유지 (전역 설정 staleTime: 5분 사용)
gcTime: 10 * 60 * 1000,
})
Expand Down
1 change: 1 addition & 0 deletions src/features/meetings/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export type {
MyMeetingRole,
MyMeetingTabCountsResponse,
RejectMeetingResponse,
RetrospectiveStatus,
UpdateMeetingRequest,
UpdateMeetingResponse,
} from './meetings.types'
10 changes: 7 additions & 3 deletions src/features/meetings/meetings.mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,10 @@ const mockMeetingDetails: Record<number, GetMeetingDetailResponse> = {
meetingName: '1차 독서모임',
meetingStatus: 'PENDING',
progressStatus: 'POST',
confirmedTopic: true,
confirmedTopicDate: '2026-01-20T14:00:00',
retrospectiveStatus: 'AI_SUMMARY_COMPLETED',
personalRetrospectiveWritten: false,
gathering: {
gatheringId: 101,
gatheringName: '클린 코드 스터디',
Expand Down Expand Up @@ -261,16 +265,16 @@ const mockMeetingDetails: Record<number, GetMeetingDetailResponse> = {
buttonLabel: '약속이 끝났어요',
enabled: false,
},
confirmedTopic: true,
confirmedTopicDate: '2026-01-20T14:00:00',
},
11: {
meetingId: 11,
progressStatus: 'PRE',
meetingName: '킥오프 모임',
meetingStatus: 'CONFIRMED',
progressStatus: 'PRE',
confirmedTopic: false,
confirmedTopicDate: null,
retrospectiveStatus: 'NOT_CREATED',
personalRetrospectiveWritten: false,
gathering: {
gatheringId: 102,
gatheringName: '실용주의 프로그래머 독서모임',
Expand Down
10 changes: 10 additions & 0 deletions src/features/meetings/meetings.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,12 @@ export type MeetingDetailActionStateType =
| 'JOIN_TIME_EXPIRED'

export type MeetingProgressStatus = 'PRE' | 'ONGOING' | 'POST'

/**
* 약속 회고 작성 상태 타입
*/
export type RetrospectiveStatus = 'NOT_CREATED' | 'AI_SUMMARY_COMPLETED' | 'FINAL_PUBLISHED'

/**
* 약속 상세 조회 응답 타입
*/
Expand All @@ -218,6 +224,10 @@ export type GetMeetingDetailResponse = {
confirmedTopic: boolean
/** 주제 확정 일시 */
confirmedTopicDate: string | null
/** 약속 회고 작성 상태 */
retrospectiveStatus: RetrospectiveStatus
/** 개인 회고 작성 여부 */
personalRetrospectiveWritten: boolean
/** 모임 정보 */
gathering: {
gatheringId: number
Expand Down
72 changes: 57 additions & 15 deletions src/features/retrospectives/components/RetrospectiveCardButtons.tsx
Original file line number Diff line number Diff line change
@@ -1,34 +1,68 @@
import { useNavigate } from 'react-router-dom'

import type { RetrospectiveStatus } from '@/features/meetings'
import meetingRetroIcon from '@/shared/assets/icon/meeting-retro.svg'
import personalRetroIcon from '@/shared/assets/icon/personal-retro.svg'
import { ROUTES } from '@/shared/constants'

interface RetrospectiveCardButtonsProps {
gatheringId: number
meetingId: number
retrospectiveStatus: RetrospectiveStatus
personalRetrospectiveWritten: boolean
}

export default function RetrospectiveCardButtons({
gatheringId,
meetingId,
retrospectiveStatus,
personalRetrospectiveWritten,
}: RetrospectiveCardButtonsProps) {
const navigate = useNavigate()

// 약속 회고 라우트 결정
const getMeetingRetrospectiveRoute = () => {
switch (retrospectiveStatus) {
case 'NOT_CREATED':
return ROUTES.MEETING_RETROSPECTIVE_CREATE(gatheringId, meetingId)
case 'FINAL_PUBLISHED':
return ROUTES.MEETING_RETROSPECTIVE_DETAIL(gatheringId, meetingId)
case 'AI_SUMMARY_COMPLETED':
return ROUTES.MEETING_RETROSPECTIVE(gatheringId, meetingId)
default:
return ROUTES.MEETING_RETROSPECTIVE_CREATE(gatheringId, meetingId)
}
}

// 개인 회고 라우트 결정 (추후 라우트 반영 예정)
const getPersonalRetrospectiveRoute = () => {
// TODO: ROUTES.PERSONAL_RETROSPECTIVE, ROUTES.PERSONAL_RETROSPECTIVE_VIEW 추가 후 활성화
if (personalRetrospectiveWritten) {
// return ROUTES.PERSONAL_RETROSPECTIVE_VIEW(gatheringId, meetingId)
return '#'
}
// return ROUTES.PERSONAL_RETROSPECTIVE(gatheringId, meetingId)
return '#'
}

return (
<div className="flex gap-small w-full">
<div className="flex gap-small w-full pb-large">
{/* 약속 회고 카드 */}
<button
type="button"
className="flex flex-1 items-center gap-base rounded-base bg-white p-large shadow-drop cursor-pointer"
onClick={() => navigate(ROUTES.MEETING_RETROSPECTIVE(gatheringId, meetingId))}
onClick={() => navigate(getMeetingRetrospectiveRoute())}
>
{/* TODO: GraphicIc meeting review 아이콘 */}
<div className="flex flex-col gap-xsmall items-start">
<div className="flex items-center gap-xsmall">
<span className="text-black typo-subtitle2">약속 회고</span>
<div className="flex gap-base">
<img src={meetingRetroIcon} alt="약속 회고" className="shrink-0" />
<div className="flex flex-col gap-xsmall items-start">
<div className="flex items-center gap-xsmall">
<span className="text-black typo-subtitle2">약속 회고</span>
</div>
<span className="text-grey-600 typo-body4">
약속에서 나눈 대화를 다같이 정리해 남겨보세요
</span>
</div>
<span className="text-grey-600 typo-body4">
약속에서 나눈 대화를 다같이 정리해 남겨보세요
</span>
</div>
</button>

Expand All @@ -37,15 +71,23 @@ export default function RetrospectiveCardButtons({
type="button"
className="flex flex-1 items-center gap-base rounded-base bg-white p-large shadow-drop cursor-pointer"
onClick={() => {
/* TODO: 개인 회고 라우트 연결 */
// TODO: ROUTES.PERSONAL_RETROSPECTIVE, ROUTES.PERSONAL_RETROSPECTIVE_VIEW 추가 후 실제 navigate 활성화
const route = getPersonalRetrospectiveRoute()
console.log('개인 회고 라우트:', route)
// navigate(route)
}}
>
{/* TODO: GraphicIc personal review 아이콘 */}
<div className="flex flex-col gap-xsmall items-start">
<div className="flex items-center gap-xsmall">
<span className="text-black typo-subtitle2">개인 회고</span>
<div className="flex gap-base">
<img src={personalRetroIcon} alt="개인 회고" className="shrink-0" />

<div className="flex flex-col gap-xsmall items-start">
<div className="flex items-center gap-xsmall">
<span className="text-black typo-subtitle2">개인 회고</span>
</div>
<span className="text-grey-600 typo-body4">
약속 후 느낀 나만의 생각을 정리해보세요
</span>
</div>
<span className="text-grey-600 typo-body4">약속 후 느낀 나만의 생각을 정리해보세요</span>
</div>
</button>
</div>
Expand Down
1 change: 1 addition & 0 deletions src/features/retrospectives/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export * from './retrospectiveQueryKeys'
export * from './useCollectedAnswers'
export * from './useCreateSttJob'
export * from './usePublishSummary'
export * from './useSummary'
Expand Down
18 changes: 18 additions & 0 deletions src/features/retrospectives/hooks/retrospectiveQueryKeys.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,24 @@
/**
* @file retrospectiveQueryKeys.ts
* @description 회고 관련 Query Key Factory
*/

import type { GetCollectedAnswersParams } from '../retrospectives.types'

/**
* Query Key Factory
*
* @description
* 회고 관련 Query Key를 일관되게 관리하기 위한 팩토리 함수
*/
export const retrospectiveQueryKeys = {
all: ['retrospectives'] as const,

// 수집된 사전 의견 리스트
collectedAnswersList: (params: Omit<GetCollectedAnswersParams, 'cursorUserId'>) =>
[...retrospectiveQueryKeys.all, 'collectedAnswers', params] as const,

// 회고 요약
summaries: () => [...retrospectiveQueryKeys.all, 'summary'] as const,
summary: (meetingId: number) => [...retrospectiveQueryKeys.summaries(), meetingId] as const,
}
61 changes: 61 additions & 0 deletions src/features/retrospectives/hooks/useCollectedAnswers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/**
* @file useCollectedAnswers.ts
* @description 수집된 사전 의견 조회 훅 (무한 스크롤)
*/

import { type InfiniteData, useInfiniteQuery } from '@tanstack/react-query'

import type { ApiError } from '@/api'
import { PAGE_SIZES } from '@/shared/constants/pagination'

import { getCollectedAnswers } from '../retrospectives.api'
import type {
CollectedAnswerCursor,
GetCollectedAnswersParams,
GetCollectedAnswersResponse,
} from '../retrospectives.types'
import { retrospectiveQueryKeys } from './retrospectiveQueryKeys'

/**
* 수집된 사전 의견 조회 훅 (무한 스크롤)
*
* @description
* TanStack Query의 useInfiniteQuery를 사용하여 약속의 수집된 사전 의견 목록을 무한 스크롤로 조회합니다.
* 사용자별로 각 주제에 대한 답변을 확인할 수 있습니다.
*
* @param params - 조회 파라미터
* @param params.meetingId - 약속 식별자
* @param params.pageSize - 페이지 크기 (기본값: 10)
*
* @returns TanStack Query 무한 스크롤 결과 객체
*/
export const useCollectedAnswers = (params: Omit<GetCollectedAnswersParams, 'cursorUserId'>) => {
const { meetingId, pageSize = PAGE_SIZES.COLLECTED_ANSWERS } = params

return useInfiniteQuery<
GetCollectedAnswersResponse,
ApiError,
InfiniteData<GetCollectedAnswersResponse>,
ReturnType<typeof retrospectiveQueryKeys.collectedAnswersList>,
CollectedAnswerCursor | null
>({
queryKey: retrospectiveQueryKeys.collectedAnswersList({ meetingId, pageSize }),
queryFn: ({ pageParam }: { pageParam: CollectedAnswerCursor | null }) =>
getCollectedAnswers({
meetingId,
pageSize,
// 첫 페이지: 커서 없이 요청 (pageParam = null), 다음 페이지: nextCursor 사용
cursorUserId: pageParam?.userId,
}),
// meetingId가 유효할 때만 쿼리 실행
enabled: meetingId > 0,
// 초기 페이지 파라미터 (첫 페이지는 커서 파라미터 없이 요청)
initialPageParam: null,
// 다음 페이지 파라미터 가져오기
getNextPageParam: (lastPage) => {
return lastPage.hasNext ? lastPage.nextCursor : null
},
// 캐시 데이터 10분간 유지
gcTime: 10 * 60 * 1000,
})
}
Loading
Loading