From 8548393852c4488982517480ba7dac1e2a8c8e01 Mon Sep 17 00:00:00 2001 From: HIHJH <2170095@ewhain.net> Date: Sun, 15 Feb 2026 13:39:55 +0900 Subject: [PATCH 01/13] =?UTF-8?q?fix:=20InterviewResultStatus=20API?= =?UTF-8?q?=EC=97=90=20=EB=A7=9E=EC=B6=B0=20=EB=8C=80=EB=AC=B8=EC=9E=90?= =?UTF-8?q?=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../my-interviews/constants/constants.ts | 50 ++++-- .../dashboard/my-interviews/example.ts | 167 ++---------------- .../dashboard/my-collections/detail/page.tsx | 8 +- .../my-collections/difficult/page.tsx | 6 +- 4 files changed, 59 insertions(+), 172 deletions(-) diff --git a/frontend/src/features/dashboard/my-interviews/constants/constants.ts b/frontend/src/features/dashboard/my-interviews/constants/constants.ts index 4acef0c10..f3c340f77 100644 --- a/frontend/src/features/dashboard/my-interviews/constants/constants.ts +++ b/frontend/src/features/dashboard/my-interviews/constants/constants.ts @@ -1,5 +1,5 @@ import type { LabelValueType } from '@/types/global' -import type { InterviewFilter } from '@/types/interview' +import type { InterviewFilter, QuestionFilter, StarLevel } from '@/types/interview' export const TAB_ITEMS: LabelValueType[] = [ { label: '면접', value: 'interviews' }, @@ -16,21 +16,49 @@ export const EMPTY_FILTER: InterviewFilter = { } export const RESULT_THEME = { - pass: 'green-100', - wait: 'orange-50', - fail: 'red-50', + PASS: 'green-100', + WAIT: 'orange-50', + FAIL: 'red-50', } as const export const RESULT_LABEL = { - pass: '합격', - wait: '발표 대기', - fail: '불합격', + PASS: '합격', + WAIT: '발표 대기', + FAIL: '불합격', } export const RESULT_STATUS_ITEMS: LabelValueType[] = [ - { label: '합격', value: 'pass' }, - { label: '발표 대기', value: 'wait' }, - { label: '불합격', value: 'fail' }, + { label: '합격', value: 'PASS' }, + { label: '발표 대기', value: 'WAIT' }, + { label: '불합격', value: 'FAIL' }, ] -export type InterviewResultStatus = 'pass' | 'wait' | 'fail' +export type InterviewResultStatus = 'PASS' | 'WAIT' | 'FAIL' + +export const EMPTY_QUESTION_FILTER: QuestionFilter = { + keyword: '', + sort: 'interviewStartAt,desc', + hasStarAnalysis: null, + sInclusionLevels: [], + tInclusionLevels: [], + aInclusionLevels: [], + rInclusionLevels: [], +} + +export const QUESTION_SORT_OPTIONS = [ + { label: '면접 일시 최신순', value: 'interviewStartAt,desc' }, + { label: '면접 일시 오래된 순', value: 'interviewStartAt,asc' }, + { label: '최신 업데이트순', value: 'updatedAt,desc' }, + { label: '가나다순', value: 'questionText,asc' }, +] as const + +export const STAR_LEVEL_OPTIONS: { label: string; value: StarLevel }[] = [ + { label: '충분', value: 'PRESENT' }, + { label: '부족', value: 'INSUFFICIENT' }, + { label: '없음', value: 'ABSENT' }, +] + +export const DATA_EMPTY_MESSAGE = { + interviews: '아직 면접 기록이 없어요. 면접을 보고 결과를 등록해서 면접 기록을 모아보세요.', + questions: '아직 질문 데이터가 없어요. 면접 기록을 진행해서 질문과 답변을 모아보세요.', +} diff --git a/frontend/src/features/dashboard/my-interviews/example.ts b/frontend/src/features/dashboard/my-interviews/example.ts index b97902681..f6418cb95 100644 --- a/frontend/src/features/dashboard/my-interviews/example.ts +++ b/frontend/src/features/dashboard/my-interviews/example.ts @@ -1,7 +1,7 @@ import type { InterviewType } from '@/types/interview' export type InterviewItemType = { - resultStatus: 'wait' | 'pass' | 'fail' + resultStatus: 'WAIT' | 'PASS' | 'FAIL' date: string company: string jobRole: string @@ -71,56 +71,56 @@ export const MOCK_DRAFTS: DraftItemType[] = [ export const MOCK_COMPLETED: InterviewItemType[] = [ { - resultStatus: 'pass', + resultStatus: 'PASS', date: '2025. 11. 29 응시', company: '현대자동차', jobRole: '데이터 사이언티스트', interviewType: 'CULTURE_FIT', }, { - resultStatus: 'wait', + resultStatus: 'WAIT', date: '2025. 11. 29 응시', company: '현대자동차', jobRole: '데이터 사이언티스트', interviewType: 'CULTURE_FIT', }, { - resultStatus: 'fail', + resultStatus: 'FAIL', date: '2025. 11. 29 응시', company: '현대자동차', jobRole: '데이터 사이언티스트', interviewType: 'CULTURE_FIT', }, { - resultStatus: 'pass', + resultStatus: 'PASS', date: '2025. 11. 29 응시', company: '현대자동차', jobRole: '데이터 사이언티스트', interviewType: 'CULTURE_FIT', }, { - resultStatus: 'fail', + resultStatus: 'FAIL', date: '2025. 11. 29 응시', company: '현대자동차', jobRole: '데이터 사이언티스트', interviewType: 'CULTURE_FIT', }, { - resultStatus: 'pass', + resultStatus: 'WAIT', date: '2025. 11. 29 응시', company: '현대자동차', jobRole: '데이터 사이언티스트', interviewType: 'CULTURE_FIT', }, { - resultStatus: 'wait', + resultStatus: 'WAIT', date: '2025. 11. 29 응시', company: '현대자동차', jobRole: '데이터 사이언티스트', interviewType: 'CULTURE_FIT', }, { - resultStatus: 'pass', + resultStatus: 'PASS', date: '2025. 11. 29 응시', company: '현대자동차', jobRole: '데이터 사이언티스트', @@ -128,150 +128,9 @@ export const MOCK_COMPLETED: InterviewItemType[] = [ }, ] -export const MOCK_QUESTION_RANKS: QuestionRankType[] = [ - { categoryId: 13, categoryName: '리더십', frequentCount: 12 }, - { categoryId: 8, categoryName: '갈등 해결', frequentCount: 9 }, - { categoryId: 21, categoryName: '협업 경험', frequentCount: 8 }, - { categoryId: 5, categoryName: '실패 경험', frequentCount: 7 }, - { categoryId: 2, categoryName: '성장 과정', frequentCount: 0 }, -] - -export const MOCK_QUESTION_PREVIEWS: Record = { - 리더십: [ - { - resultStatus: 'pass', - date: '2025. 11. 29', - company: '현대자동차', - jobRole: '데이터 사이언티스트', - interviewType: 'CULTURE_FIT', - question: '팀에서 리더 역할을 맡았던 경험이 있나요? 그때 어떻게 팀을 이끌었는지 말씀해주세요.', - }, - { - resultStatus: 'wait', - date: '2025. 11. 25', - company: '카카오', - jobRole: '프론트엔드 개발자', - interviewType: 'FIRST', - question: '리더십을 발휘해서 프로젝트를 성공적으로 이끈 경험을 말씀해주세요.', - }, - { - resultStatus: 'pass', - date: '2025. 11. 20', - company: '네이버', - jobRole: '서비스 기획', - interviewType: 'BEHAVIORAL', - question: '팀원 간 의견이 충돌할 때 리더로서 어떻게 해결하셨나요?', - }, - ], - '갈등 해결': [ - { - resultStatus: 'fail', - date: '2025. 11. 28', - company: '토스', - jobRole: '백엔드 개발자', - interviewType: 'BEHAVIORAL', - question: '팀 내 갈등이 발생했을 때 어떻게 중재하셨나요?', - }, - { - resultStatus: 'pass', - date: '2025. 11. 22', - company: '카카오', - jobRole: '서비스 기획', - interviewType: 'CULTURE_FIT', - question: '상사와 의견이 다를 때 어떻게 대처하시나요?', - }, - { - resultStatus: 'wait', - date: '2025. 11. 18', - company: '네이버', - jobRole: 'PM', - interviewType: 'SECOND', - question: '고객과 갈등 상황을 해결한 경험을 말씀해주세요.', - }, - ], - '협업 경험': [ - { - resultStatus: 'pass', - date: '2025. 11. 27', - company: '라인', - jobRole: '프론트엔드 개발자', - interviewType: 'TECHNICAL', - question: '다른 팀과 협업하여 프로젝트를 완수한 경험이 있나요?', - }, - { - resultStatus: 'pass', - date: '2025. 11. 21', - company: '쿠팡', - jobRole: '데이터 엔지니어', - interviewType: 'FIRST', - question: '비개발 직군과 협업할 때 소통 방식을 설명해주세요.', - }, - { - resultStatus: 'fail', - date: '2025. 11. 15', - company: '배민', - jobRole: '백엔드 개발자', - interviewType: 'CULTURE_FIT', - question: '협업 중 가장 어려웠던 점과 극복 방법을 알려주세요.', - }, - ], - '실패 경험': [ - { - resultStatus: 'wait', - date: '2025. 11. 26', - company: '현대자동차', - jobRole: '서비스 기획', - interviewType: 'EXECUTIVE', - question: '가장 큰 실패 경험과 그로부터 배운 점을 말씀해주세요.', - }, - { - resultStatus: 'pass', - date: '2025. 11. 19', - company: '삼성전자', - jobRole: 'SW 개발자', - interviewType: 'BEHAVIORAL', - question: '프로젝트가 실패한 적이 있나요? 어떻게 대처하셨나요?', - }, - { - resultStatus: 'fail', - date: '2025. 11. 12', - company: 'SK하이닉스', - jobRole: '데이터 분석가', - interviewType: 'FIRST', - question: '실패를 통해 성장한 경험을 구체적으로 설명해주세요.', - }, - ], - '성장 과정': [ - { - resultStatus: 'pass', - date: '2025. 11. 24', - company: '토스', - jobRole: '프론트엔드 개발자', - interviewType: 'TECHNICAL', - question: '최근 1년간 가장 크게 성장한 부분은 무엇인가요?', - }, - { - resultStatus: 'pass', - date: '2025. 11. 17', - company: '카카오', - jobRole: '백엔드 개발자', - interviewType: 'CULTURE_FIT', - question: '자기 개발을 위해 어떤 노력을 하고 계신가요?', - }, - { - resultStatus: 'wait', - date: '2025. 11. 10', - company: '네이버', - jobRole: 'PM', - interviewType: 'BEHAVIORAL', - question: '커리어에서 전환점이 된 경험을 말씀해주세요.', - }, - ], -} - export const MOCK_QNA: QnaItemType[] = [ { - resultStatus: 'pass', + resultStatus: 'PASS', date: '2025. 11. 29 응시', company: '현대자동차', jobRole: '데이터 사이언티스트', @@ -280,7 +139,7 @@ export const MOCK_QNA: QnaItemType[] = [ answer: '대학교 졸업 프로젝트에서 팀장을 맡아 5명의 팀원들과 함께 AI 기반 추천 시스템을 개발했습니다.', }, { - resultStatus: 'wait', + resultStatus: 'WAIT', date: '2025. 11. 29 응시', company: '카카오', jobRole: '프론트엔드 개발자', @@ -290,7 +149,7 @@ export const MOCK_QNA: QnaItemType[] = [ '실시간 채팅 서비스에서 WebSocket 연결이 끊기는 문제를 해결하기 위해 재연결 로직과 메시지 큐를 구현했습니다.', }, { - resultStatus: 'fail', + resultStatus: 'FAIL', date: '2025. 11. 25 응시', company: '네이버', jobRole: '서비스 기획', @@ -299,7 +158,7 @@ export const MOCK_QNA: QnaItemType[] = [ answer: '먼저 상대방의 의견을 충분히 경청한 후, 공통점을 찾아 합의점을 도출하려고 노력합니다.', }, { - resultStatus: 'pass', + resultStatus: 'PASS', date: '2025. 11. 20 응시', company: '토스', jobRole: '백엔드 개발자', diff --git a/frontend/src/pages/dashboard/my-collections/detail/page.tsx b/frontend/src/pages/dashboard/my-collections/detail/page.tsx index d830748f8..78b38e180 100644 --- a/frontend/src/pages/dashboard/my-collections/detail/page.tsx +++ b/frontend/src/pages/dashboard/my-collections/detail/page.tsx @@ -12,7 +12,7 @@ const MOCK_QUESTIONS: QnaCardListItem[] = [ interviewType: 'CULTURE_FIT', question: '팀에서 리더 역할을 맡았던 경험이 있나요?', answer: '대학교 졸업 프로젝트에서 팀장을 맡아 5명의 팀원들과 함께 AI 기반 추천 시스템을 개발했습니다.', - resultStatus: 'pass', + resultStatus: 'PASS', }, { id: 2, @@ -23,7 +23,7 @@ const MOCK_QUESTIONS: QnaCardListItem[] = [ question: '가장 어려웠던 기술적 문제와 해결 과정을 설명해주세요.', answer: '실시간 채팅 서비스에서 WebSocket 연결이 끊기는 문제를 해결하기 위해 재연결 로직과 메시지 큐를 구현했습니다.', - resultStatus: 'wait', + resultStatus: 'WAIT', }, { id: 3, @@ -33,7 +33,7 @@ const MOCK_QUESTIONS: QnaCardListItem[] = [ interviewType: 'BEHAVIORAL', question: '협업 과정에서 갈등이 생겼을 때 어떻게 해결하시나요?', answer: '먼저 상대방의 의견을 충분히 경청한 후, 공통점을 찾아 합의점을 도출하려고 노력합니다.', - resultStatus: 'pass', + resultStatus: 'PASS', }, { id: 4, @@ -43,7 +43,7 @@ const MOCK_QUESTIONS: QnaCardListItem[] = [ interviewType: 'TECHNICAL', question: 'RESTful API 설계 원칙에 대해 설명해주세요.', answer: 'REST는 자원을 URI로 표현하고, HTTP 메서드를 통해 자원에 대한 행위를 정의하는 아키텍처 스타일입니다.', - resultStatus: 'fail', + resultStatus: 'FAIL', }, ] diff --git a/frontend/src/pages/dashboard/my-collections/difficult/page.tsx b/frontend/src/pages/dashboard/my-collections/difficult/page.tsx index b34f57ed5..8b12a0d60 100644 --- a/frontend/src/pages/dashboard/my-collections/difficult/page.tsx +++ b/frontend/src/pages/dashboard/my-collections/difficult/page.tsx @@ -10,7 +10,7 @@ const MOCK_DIFFICULT_QUESTIONS: (QnaCardListItem & { tags?: readonly { type: str job: '백엔드 개발', interviewType: 'SECOND', question: '대규모 트래픽 발생 시 부하 분산 전략에 대해 설명해주세요.', - resultStatus: 'pass', + resultStatus: 'PASS', tags: [ { type: 'pass', text: '합격' }, { type: 'practice', text: '실전면접' }, @@ -23,7 +23,7 @@ const MOCK_DIFFICULT_QUESTIONS: (QnaCardListItem & { tags?: readonly { type: str job: '서버 개발', interviewType: 'FIRST', question: 'Database Deadlock이 발생하는 원인과 해결 방안은 무엇인가요?', - resultStatus: 'wait', + resultStatus: 'WAIT', tags: [{ type: 'practice', text: '모의면접' }], }, { @@ -33,7 +33,7 @@ const MOCK_DIFFICULT_QUESTIONS: (QnaCardListItem & { tags?: readonly { type: str job: '플랫폼 엔지니어링', interviewType: 'TECHNICAL', question: 'MSA 환경에서 트랜잭션 관리를 어떻게 수행해야 하나요?', - resultStatus: 'pass', + resultStatus: 'PASS', tags: [{ type: 'pass', text: '합격' }], }, ] From 5620609f5f8d72919d598399320dc7e7b6870a84 Mon Sep 17 00:00:00 2001 From: HIHJH <2170095@ewhain.net> Date: Sun, 15 Feb 2026 13:43:26 +0900 Subject: [PATCH 02/13] =?UTF-8?q?fix:=20=EC=8A=A4=EC=9B=A8=EA=B1=B0=20quer?= =?UTF-8?q?y=20param=20pageable=20=EA=B0=9D=EC=B2=B4=EB=A1=9C=20=EB=AC=B6?= =?UTF-8?q?=EC=97=AC=EC=9E=88=EB=8A=94=20=EC=9D=B4=EC=8A=88=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20=EC=A0=84=20=EC=9E=84=EC=8B=9C=20=EB=8C=80=EC=B2=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/apis/generated/refit-api.schemas.ts | 60 +++++++++++++++---- 1 file changed, 48 insertions(+), 12 deletions(-) diff --git a/frontend/src/apis/generated/refit-api.schemas.ts b/frontend/src/apis/generated/refit-api.schemas.ts index c85296537..a71d3f4da 100644 --- a/frontend/src/apis/generated/refit-api.schemas.ts +++ b/frontend/src/apis/generated/refit-api.schemas.ts @@ -931,15 +931,24 @@ export type SignUpParams = { } export type GetMyScrapFoldersParams = { - pageable: Pageable + pageable?: Pageable + page?: number + size?: number + sort?: string[] } export type SearchMyQnaSetParams = { - pageable: Pageable + pageable?: Pageable + page?: number + size?: number + sort?: string[] } export type SearchInterviewsParams = { - pageable: Pageable + pageable?: Pageable + page?: number + size?: number + sort?: string[] } export type PublishTokenParams = { @@ -952,30 +961,48 @@ export type PublishTokenByUserIdParams = { } export type GetQnaSetsInScrapFolderParams = { - pageable: Pageable + pageable?: Pageable + page?: number + size?: number + sort?: string[] } export type GetScrapFoldersContainingQnaSetParams = { - pageable: Pageable + pageable?: Pageable + page?: number + size?: number + sort?: string[] } export type GetMyFrequentQnaSetCategoriesParams = { - pageable: Pageable + pageable?: Pageable + page?: number + size?: number + sort?: string[] } export type GetMyFrequentQnaSetCategoryQuestionsParams = { - pageable: Pageable + pageable?: Pageable + page?: number + size?: number + sort?: string[] } export type GetFrequentQuestionsParams = { industryIds?: number[] jobCategoryIds?: number[] - pageable: Pageable + pageable?: Pageable + page?: number + size?: number + sort?: string[] } export type GetMyInterviewDraftsParams = { interviewReviewStatus: GetMyInterviewDraftsInterviewReviewStatus - pageable: Pageable + pageable?: Pageable + page?: number + size?: number + sort?: string[] } export type GetMyInterviewDraftsInterviewReviewStatus = @@ -989,15 +1016,24 @@ export const GetMyInterviewDraftsInterviewReviewStatus = { } as const export type GetMyDifficultQnaSetsParams = { - pageable: Pageable + pageable?: Pageable + page?: number + size?: number + sort?: string[] } export type GetUpcomingInterviewsParams = { - pageable: Pageable + pageable?: Pageable + page?: number + size?: number + sort?: string[] } export type GetDebriefIncompletedInterviewsParams = { - pageable: Pageable + pageable?: Pageable + page?: number + size?: number + sort?: string[] } export type GetDashboardCalendarInterviewsParams = { From 8eea33b33e2af8b22edd3f2fe60ad518b4763d1b Mon Sep 17 00:00:00 2001 From: HIHJH <2170095@ewhain.net> Date: Sun, 15 Feb 2026 13:50:20 +0900 Subject: [PATCH 03/13] =?UTF-8?q?fix:=20pageable=20param=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/features/retro/_common/components/ScrapModal.tsx | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/frontend/src/features/retro/_common/components/ScrapModal.tsx b/frontend/src/features/retro/_common/components/ScrapModal.tsx index ca4096d79..edf6fb6e2 100644 --- a/frontend/src/features/retro/_common/components/ScrapModal.tsx +++ b/frontend/src/features/retro/_common/components/ScrapModal.tsx @@ -11,11 +11,7 @@ export type ScrapModalProps = { } export function ScrapModal({ isOpen, onClose, qnaSetId }: ScrapModalProps) { - const { data } = useGetScrapFoldersContainingQnaSet( - qnaSetId, - { pageable: { page: 0, size: 5 } }, - { query: { enabled: isOpen } }, - ) + const { data } = useGetScrapFoldersContainingQnaSet(qnaSetId, { page: 0, size: 5 }, { query: { enabled: isOpen } }) const folders = data?.result?.content ?? [] const [selectedIds, setSelectedIds] = useState>(new Set()) From 3c464782ad42027ca92ec654dadf02d54159b971 Mon Sep 17 00:00:00 2001 From: HIHJH <2170095@ewhain.net> Date: Sun, 15 Feb 2026 13:50:39 +0900 Subject: [PATCH 04/13] =?UTF-8?q?feat:=20formatDate=20=ED=95=A8=EC=88=98?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/sidebar/InterviewInfoSection.tsx | 2 +- frontend/src/features/_common/utils/date.ts | 10 +++++++++- frontend/src/features/_common/utils/index.ts | 1 - 3 files changed, 10 insertions(+), 3 deletions(-) delete mode 100644 frontend/src/features/_common/utils/index.ts diff --git a/frontend/src/features/_common/components/sidebar/InterviewInfoSection.tsx b/frontend/src/features/_common/components/sidebar/InterviewInfoSection.tsx index 492aa6222..10e52ceef 100644 --- a/frontend/src/features/_common/components/sidebar/InterviewInfoSection.tsx +++ b/frontend/src/features/_common/components/sidebar/InterviewInfoSection.tsx @@ -1,6 +1,6 @@ import { INTERVIEW_TYPE_LABEL } from '@/constants/interviews' import { ContainerWithoutHeader } from '@/designs/components' -import { formatDateTime } from '@/features/_common/utils' +import { formatDateTime } from '@/features/_common/utils/date' import type { LabelValueType } from '@/types/global' import type { InterviewInfoType } from '@/types/interview' diff --git a/frontend/src/features/_common/utils/date.ts b/frontend/src/features/_common/utils/date.ts index 5006bc8bf..a0e06716c 100644 --- a/frontend/src/features/_common/utils/date.ts +++ b/frontend/src/features/_common/utils/date.ts @@ -1,4 +1,4 @@ -export default function formatDateTime(dateString: string): string { +export function formatDateTime(dateString: string): string { return new Date(dateString).toLocaleString('ko-KR', { year: 'numeric', month: 'long', @@ -8,3 +8,11 @@ export default function formatDateTime(dateString: string): string { hour12: false, }) } + +export function formatDate(dateString: string): string { + return new Date(dateString).toLocaleDateString('ko-KR', { + year: 'numeric', + month: 'long', + day: 'numeric', + }) +} diff --git a/frontend/src/features/_common/utils/index.ts b/frontend/src/features/_common/utils/index.ts deleted file mode 100644 index 457bfe795..000000000 --- a/frontend/src/features/_common/utils/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default as formatDateTime } from './date' From 70711457dd7a0b1dd80638b45ed6b7c787597d77 Mon Sep 17 00:00:00 2001 From: HIHJH <2170095@ewhain.net> Date: Sun, 15 Feb 2026 13:51:57 +0900 Subject: [PATCH 05/13] =?UTF-8?q?feat:=20=EB=82=B4=20=EC=A7=88=EB=AC=B8=20?= =?UTF-8?q?=EB=AA=A8=EC=95=84=EB=B3=B4=EA=B8=B0=20mock-api=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/mocks/browser.ts | 26 ++ .../src/mocks/data/my-frequent-questions.ts | 246 ++++++++++++++++++ 2 files changed, 272 insertions(+) create mode 100644 frontend/src/mocks/data/my-frequent-questions.ts diff --git a/frontend/src/mocks/browser.ts b/frontend/src/mocks/browser.ts index 009d5f476..07fcd1b49 100644 --- a/frontend/src/mocks/browser.ts +++ b/frontend/src/mocks/browser.ts @@ -18,14 +18,40 @@ import { getUpdatePdfHighlightingMockHandler, getUpdateQnaSetMockHandler, } from '@/apis/generated/qna-set-api/qna-set-api.msw' +import { + getSearchMyQnaSetMockHandler, + getGetMyFrequentQnaSetCategoriesMockHandler, + getGetMyFrequentQnaSetCategoryQuestionsMockHandler, +} from '@/apis/generated/qna-set-my-controller/qna-set-my-controller.msw' +import type { QnaSetSearchRequest } from '@/apis/generated/refit-api.schemas' import { getCreateScrapFolderMockHandler } from '@/apis/generated/scrap-folder-api/scrap-folder-api.msw' import { debriefIncompletedMock } from '@/mocks/data/debrief-incompleted' import { mockInterviewFull } from '@/mocks/data/interview-full' +import { + getMockFrequentCategoryQuestions, + getMockSearchMyQnaSet, + mockFrequentQuestionCategories, +} from '@/mocks/data/my-frequent-questions' import { mockScrapFolders } from '@/mocks/data/scrap-folders' import { mockStarAnalysis } from '@/mocks/data/star-analysis' import { updateRawTextMock } from '@/mocks/data/update-raw-text' export const worker = setupWorker( + getGetMyFrequentQnaSetCategoriesMockHandler(mockFrequentQuestionCategories), + getGetMyFrequentQnaSetCategoryQuestionsMockHandler((info) => { + const categoryId = Number(info.params.categoryId) + const url = new URL(info.request.url) + const page = Number(url.searchParams.get('page') ?? '0') + const size = Number(url.searchParams.get('size') ?? '9') + return getMockFrequentCategoryQuestions(categoryId, page, size) + }), + getSearchMyQnaSetMockHandler(async (info) => { + const url = new URL(info.request.url) + const page = Number(url.searchParams.get('page') ?? '0') + const size = Number(url.searchParams.get('size') ?? '9') + const request = (await info.request.json().catch(() => ({}))) as QnaSetSearchRequest + return getMockSearchMyQnaSet({ page, size, request }) + }), getGetMyDifficultQnaSetsMockHandler(), getGetUpcomingInterviewsMockHandler(), getGetDebriefIncompletedInterviewsMockHandler(debriefIncompletedMock), diff --git a/frontend/src/mocks/data/my-frequent-questions.ts b/frontend/src/mocks/data/my-frequent-questions.ts new file mode 100644 index 000000000..7e9e9b412 --- /dev/null +++ b/frontend/src/mocks/data/my-frequent-questions.ts @@ -0,0 +1,246 @@ +import type { + ApiResponsePageFrequentQnaSetCategoryQuestionResponse, + ApiResponsePageFrequentQnaSetCategoryResponse, + ApiResponsePageQnaSetSearchResponse, + FrequentQnaSetCategoryQuestionResponse, + FrequentQnaSetCategoryResponse, + QnaSetSearchRequest, + QnaSetSearchResponse, +} from '@/apis/generated/refit-api.schemas' + +const categories: FrequentQnaSetCategoryResponse[] = [ + { categoryId: 101, categoryName: '협업', frequentCount: 12, cohesion: 0.92 }, + { categoryId: 102, categoryName: '문제 해결', frequentCount: 9, cohesion: 0.88 }, + { categoryId: 103, categoryName: '리더십', frequentCount: 7, cohesion: 0.8 }, + { categoryId: 104, categoryName: '갈등 해결', frequentCount: 6, cohesion: 0.76 }, +] + +const questionsByCategoryId: Record = { + 101: [ + { + question: + '디자이너/개발자와 협업 중 의견이 달랐을 때 어떻게 정리했나요? 어떻게 정리했나요?어떻게 정리했나요?어떻게 정리했나요?', + interviewInfo: { + interviewId: 5001, + interviewType: 'TECHNICAL', + interviewStartAt: '2026-01-12T01:00:00Z', + companyInfo: { companyId: 1, companyName: '카카오헬스케어', companyLogoUrl: '' }, + jobCategoryName: '프론트엔드', + updatedAt: '2026-01-12T03:40:00Z', + }, + }, + { + question: '일정이 촉박한 프로젝트에서 팀과 우선순위를 어떻게 맞췄나요?', + interviewInfo: { + interviewId: 5002, + interviewType: 'BEHAVIORAL', + interviewStartAt: '2025-12-21T01:00:00Z', + companyInfo: { companyId: 2, companyName: '네이버', companyLogoUrl: '' }, + jobCategoryName: '웹 개발', + updatedAt: '2025-12-21T05:10:00Z', + }, + }, + { + question: '일정이 촉박한 프로젝트에서 팀과 우선순위를 어떻게 맞췄나요?', + interviewInfo: { + interviewId: 5003, + interviewType: 'BEHAVIORAL', + interviewStartAt: '2025-12-21T01:00:00Z', + companyInfo: { companyId: 2, companyName: '네이버', companyLogoUrl: '' }, + jobCategoryName: '웹 개발', + updatedAt: '2025-12-21T05:10:00Z', + }, + }, + { + question: '일정이 촉박한 프로젝트에서 팀과 우선순위를 어떻게 맞췄나요?', + interviewInfo: { + interviewId: 5004, + interviewType: 'BEHAVIORAL', + interviewStartAt: '2025-12-21T01:00:00Z', + companyInfo: { companyId: 2, companyName: '네이버', companyLogoUrl: '' }, + jobCategoryName: '웹 개발', + updatedAt: '2025-12-21T05:10:00Z', + }, + }, + ], + 102: [ + { + question: '프로덕션 이슈를 발견했을 때 원인 분석부터 복구까지 설명해 주세요.', + interviewInfo: { + interviewId: 5101, + interviewType: 'TECHNICAL', + interviewStartAt: '2026-01-03T01:00:00Z', + companyInfo: { companyId: 3, companyName: '토스', companyLogoUrl: '' }, + jobCategoryName: '프론트엔드', + updatedAt: '2026-01-03T07:20:00Z', + }, + }, + { + question: '가장 어려웠던 버그를 해결한 과정을 단계별로 설명해 주세요.', + interviewInfo: { + interviewId: 5102, + interviewType: 'FIRST', + interviewStartAt: '2025-11-30T01:00:00Z', + companyInfo: { companyId: 4, companyName: '당근', companyLogoUrl: '' }, + jobCategoryName: '앱 개발', + updatedAt: '2025-11-30T03:30:00Z', + }, + }, + ], + 103: [ + { + question: '팀을 이끌어 목표를 달성했던 경험이 있다면 말씀해 주세요.', + interviewInfo: { + interviewId: 5201, + interviewType: 'CULTURE_FIT', + interviewStartAt: '2025-12-16T01:00:00Z', + companyInfo: { companyId: 5, companyName: '라인', companyLogoUrl: '' }, + jobCategoryName: '서비스 개발', + updatedAt: '2025-12-16T04:12:00Z', + }, + }, + ], + 104: [ + { + question: '팀 내부 갈등 상황에서 본인이 취한 행동과 결과를 설명해 주세요.', + interviewInfo: { + interviewId: 5301, + interviewType: 'BEHAVIORAL', + interviewStartAt: '2025-10-19T01:00:00Z', + companyInfo: { companyId: 6, companyName: '쿠팡', companyLogoUrl: '' }, + jobCategoryName: '프론트엔드', + updatedAt: '2025-10-19T08:10:00Z', + }, + }, + ], +} + +export const mockFrequentQuestionCategories: ApiResponsePageFrequentQnaSetCategoryResponse = { + isSuccess: true, + code: 'SUCCESS', + message: 'mock frequent categories', + result: { + totalElements: categories.length, + totalPages: 1, + size: categories.length, + content: categories, + number: 0, + first: true, + numberOfElements: categories.length, + last: true, + empty: categories.length === 0, + }, +} + +export const getMockFrequentCategoryQuestions = ( + categoryId: number, + page = 0, + size = 9, +): ApiResponsePageFrequentQnaSetCategoryQuestionResponse => { + const all = questionsByCategoryId[categoryId] ?? [] + const start = page * size + const end = start + size + const paged = all.slice(start, end) + + return { + isSuccess: true, + code: 'SUCCESS', + message: 'mock category questions', + result: { + totalElements: all.length, + totalPages: Math.max(1, Math.ceil(all.length / size)), + size, + content: paged, + number: page, + first: page === 0, + numberOfElements: paged.length, + last: end >= all.length, + empty: paged.length === 0, + }, + } +} + +const searchableQuestions: QnaSetSearchResponse[] = Object.values(questionsByCategoryId) + .flat() + .map((item, index) => ({ + interviewInfo: { + interviewId: item.interviewInfo?.interviewId ?? index + 1, + interviewType: item.interviewInfo?.interviewType ?? 'FIRST', + interviewResultStatus: 'PASS', + interviewRawText: '', + companyName: item.interviewInfo?.companyInfo?.companyName ?? '', + jobCategoryId: 1, + jobCategoryName: item.interviewInfo?.jobCategoryName ?? '', + updatedAt: item.interviewInfo?.updatedAt ?? '', + createdAt: item.interviewInfo?.updatedAt ?? '', + }, + qnaSetInfo: { + qnaSetId: index + 1000, + questionText: item.question ?? '', + answerText: 'mock answer', + }, + })) + +const starAnalysisByQnaSetId: Record = { + 1000: { s: 'PRESENT', t: 'PRESENT', a: 'INSUFFICIENT', r: 'ABSENT' }, + 1001: { s: 'INSUFFICIENT', t: 'PRESENT', a: 'PRESENT', r: 'INSUFFICIENT' }, + 1002: null, + 1003: { s: 'ABSENT', t: 'INSUFFICIENT', a: 'PRESENT', r: 'PRESENT' }, + 1004: { s: 'PRESENT', t: 'PRESENT', a: 'PRESENT', r: 'PRESENT' }, + 1005: null, +} + +type SearchOptions = { + page: number + size: number + request: QnaSetSearchRequest +} + +export const getMockSearchMyQnaSet = ({ page, size, request }: SearchOptions): ApiResponsePageQnaSetSearchResponse => { + const keyword = request.keyword?.trim().toLowerCase() ?? '' + const searchFilter = request.searchFilter + + const filteredByStar = searchableQuestions.filter((item) => { + const qnaSetId = item.qnaSetInfo?.qnaSetId ?? -1 + const star = starAnalysisByQnaSetId[qnaSetId] ?? null + + if (searchFilter?.hasStarAnalysis === true && star === null) return false + if (searchFilter?.hasStarAnalysis === false && star !== null) return false + + if (searchFilter?.sInclusionLevels?.length && (!star || !searchFilter.sInclusionLevels.includes(star.s as never))) + return false + if (searchFilter?.tInclusionLevels?.length && (!star || !searchFilter.tInclusionLevels.includes(star.t as never))) + return false + if (searchFilter?.aInclusionLevels?.length && (!star || !searchFilter.aInclusionLevels.includes(star.a as never))) + return false + if (searchFilter?.rInclusionLevels?.length && (!star || !searchFilter.rInclusionLevels.includes(star.r as never))) + return false + + return true + }) + + const filtered = keyword + ? filteredByStar.filter((item) => (item.qnaSetInfo?.questionText ?? '').toLowerCase().includes(keyword)) + : filteredByStar + + const start = page * size + const end = start + size + const content = filtered.slice(start, end) + + return { + isSuccess: true, + code: 'SUCCESS', + message: 'mock search my qna set', + result: { + totalElements: filtered.length, + totalPages: Math.max(1, Math.ceil(filtered.length / size)), + size, + content, + number: page, + first: page === 0, + numberOfElements: content.length, + last: end >= filtered.length, + empty: content.length === 0, + }, + } +} From 1fcd59e64203df5d7a5e1bf10c9b4d0f6bfbceb3 Mon Sep 17 00:00:00 2001 From: HIHJH <2170095@ewhain.net> Date: Sun, 15 Feb 2026 14:02:44 +0900 Subject: [PATCH 06/13] =?UTF-8?q?feat:=20page=EC=97=90=EC=84=9C=EB=8A=94?= =?UTF-8?q?=20=ED=83=AD=20=EC=83=81=ED=83=9C=EB=A7=8C=20=EA=B4=80=EB=A6=AC?= =?UTF-8?q?=ED=95=98=EB=8F=84=EB=A1=9D=20=EC=A7=88=EB=AC=B8=ED=83=AD=20?= =?UTF-8?q?=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/questions/QuestionsTab.tsx | 32 ++++++ .../pages/dashboard/my-interviews/page.tsx | 100 ++++++++---------- frontend/src/types/interview.d.ts | 26 +++++ 3 files changed, 101 insertions(+), 57 deletions(-) create mode 100644 frontend/src/features/dashboard/my-interviews/components/questions/QuestionsTab.tsx diff --git a/frontend/src/features/dashboard/my-interviews/components/questions/QuestionsTab.tsx b/frontend/src/features/dashboard/my-interviews/components/questions/QuestionsTab.tsx new file mode 100644 index 000000000..854b31e12 --- /dev/null +++ b/frontend/src/features/dashboard/my-interviews/components/questions/QuestionsTab.tsx @@ -0,0 +1,32 @@ +import { useState } from 'react' +import { SearchBar, SearchResultBar } from '@/features/dashboard/my-interviews/components/filter' +import FrequentQuestionsSection from '@/features/dashboard/my-interviews/components/questions/frequent/FrequentQuestionsSection' +import QuestionFilterControls from '@/features/dashboard/my-interviews/components/questions/list/filter/QuestionFilterControls' +import QuestionListSection from '@/features/dashboard/my-interviews/components/questions/list/QuestionListSection' +import { EMPTY_QUESTION_FILTER } from '@/features/dashboard/my-interviews/constants/constants' +import type { QuestionFilter } from '@/types/interview' + +export default function QuestionsTab() { + const [filter, setFilter] = useState(EMPTY_QUESTION_FILTER) + const isSearching = filter.keyword.length > 0 + + return ( + <> +
+ setFilter((prev) => ({ ...prev, keyword }))} /> +
+ {!isSearching && } +
+
+ {isSearching ? ( + setFilter((prev) => ({ ...prev, keyword: '' }))} /> + ) : ( +

내가 복기 완료한 질문과 답변

+ )} + +
+ +
+ + ) +} diff --git a/frontend/src/pages/dashboard/my-interviews/page.tsx b/frontend/src/pages/dashboard/my-interviews/page.tsx index 0f9893857..60944acf1 100644 --- a/frontend/src/pages/dashboard/my-interviews/page.tsx +++ b/frontend/src/pages/dashboard/my-interviews/page.tsx @@ -3,20 +3,23 @@ import { FileSaveIcon } from '@/designs/assets' import TabBar from '@/designs/components/tab' import { FilterSortControls, SearchBar, SearchResultBar } from '@/features/dashboard/my-interviews/components/filter' import { DraftSection, InterviewListSection } from '@/features/dashboard/my-interviews/components/interviews' -import { FrequentQuestionsSection, QuestionListSection } from '@/features/dashboard/my-interviews/components/questions' +import QuestionsTab from '@/features/dashboard/my-interviews/components/questions/QuestionsTab' import { TAB_ITEMS, EMPTY_FILTER } from '@/features/dashboard/my-interviews/constants/constants' import type { InterviewFilter } from '@/types/interview' export default function MyInterviewsPage() { const [activeTab, setActiveTab] = useState<'interviews' | 'questions'>('interviews') - const [filter, setFilter] = useState(EMPTY_FILTER) - const isSearching = filter.keyword.length > 0 + { + /* TODO: interview 탭 컴포넌트 분리 */ + } + const [interviewFilter, setInterviewFilter] = useState(EMPTY_FILTER) + + const isInterviewSearching = interviewFilter.keyword.length > 0 const handleTabChange = (value: string) => { if (value !== 'interviews' && value !== 'questions') return setActiveTab(value) - setFilter(EMPTY_FILTER) } return ( @@ -25,64 +28,47 @@ export default function MyInterviewsPage() {

나의 면접을 모아볼까요?

-
- -
- setFilter((prev) => ({ ...prev, keyword }))} - /> +
+
+
-
- {activeTab === 'interviews' && ( - <> - {!isSearching && ( + {/* TODO: interview 탭 컴포넌트 분리 */} + {activeTab === 'interviews' && ( + <> +
+ setInterviewFilter((prev) => ({ ...prev, keyword }))} + /> +
+ {!isInterviewSearching && ( +
+

임시저장 항목

+
+ + +
+
+ )}
-

임시저장 항목

-
- - +
+ {isInterviewSearching ? ( + setInterviewFilter((prev) => ({ ...prev, keyword: '' }))} + /> + ) : ( +

내가 복기 완료한 면접

+ )} +
+
- )} -
-
- {isSearching ? ( - setFilter((prev) => ({ ...prev, keyword: '' }))} - /> - ) : ( -

내가 복기 완료한 면접

- )} - -
- -
- - )} + + )} - {activeTab === 'questions' && ( - <> - {!isSearching && } -
-
- {isSearching ? ( - setFilter((prev) => ({ ...prev, keyword: '' }))} - /> - ) : ( -

내가 복기 완료한 질문과 답변

- )} -
- -
-
- -
- - )} + {activeTab === 'questions' && } +
) } diff --git a/frontend/src/types/interview.d.ts b/frontend/src/types/interview.d.ts index e1b8eff46..853a34724 100644 --- a/frontend/src/types/interview.d.ts +++ b/frontend/src/types/interview.d.ts @@ -1,3 +1,10 @@ +import type { + QnaSetSearchRequest, + QnaSearchFilterAInclusionLevelsItem, + QnaSearchFilterRInclusionLevelsItem, + QnaSearchFilterSInclusionLevelsItem, + QnaSearchFilterTInclusionLevelsItem, +} from '@/apis/generated/refit-api.schemas' import type { INTERVIEW_TYPE_LABEL } from '@/constants/interviews' export type SimpleQnaType = { @@ -17,6 +24,7 @@ export type QnaSetType = { isMarkedDifficult: boolean } +// TODO: 회고 페이지에서 StarLevel로 변경 export type StarStatus = 'present' | 'insufficient' | 'absent' export type StarAnalysisResult = { @@ -62,3 +70,21 @@ type KptTextsType = { problemText: string tryText: string } + +export type StarLevel = + | QnaSearchFilterSInclusionLevelsItem + | QnaSearchFilterTInclusionLevelsItem + | QnaSearchFilterAInclusionLevelsItem + | QnaSearchFilterRInclusionLevelsItem + +type ApiQuestionSearchFilter = NonNullable + +export type QuestionFilter = { + keyword: QnaSetSearchRequest['keyword'] extends string | undefined ? string : never + sort: string + hasStarAnalysis: ApiQuestionSearchFilter['hasStarAnalysis'] | null + sInclusionLevels: NonNullable + tInclusionLevels: NonNullable + aInclusionLevels: NonNullable + rInclusionLevels: NonNullable +} From cd950d2966bb0a2e87dfb86c90cd4b23f191c76c Mon Sep 17 00:00:00 2001 From: HIHJH <2170095@ewhain.net> Date: Sun, 15 Feb 2026 14:03:46 +0900 Subject: [PATCH 07/13] =?UTF-8?q?feat:=20select,=20flatmap=EC=97=90=20?= =?UTF-8?q?=EC=9D=B4=EC=9A=A9=EB=90=98=EB=8A=94=20mapper=20=ED=95=A8?= =?UTF-8?q?=EC=88=98=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/questions/mappers.ts | 63 +++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 frontend/src/features/dashboard/my-interviews/components/questions/mappers.ts diff --git a/frontend/src/features/dashboard/my-interviews/components/questions/mappers.ts b/frontend/src/features/dashboard/my-interviews/components/questions/mappers.ts new file mode 100644 index 000000000..4d0931984 --- /dev/null +++ b/frontend/src/features/dashboard/my-interviews/components/questions/mappers.ts @@ -0,0 +1,63 @@ +import type { + FrequentQnaSetCategoryQuestionResponse, + FrequentQnaSetCategoryResponse, + InterviewSearchFilterInterviewResultStatusItem, + QnaSetSearchResponse, +} from '@/apis/generated/refit-api.schemas' +import type { InterviewType } from '@/types/interview' + +export type FrequentQuestionCategory = { + categoryId: number + categoryName: string + frequentCount: number +} + +export type QuestionCardModel = { + question: string + company: string + companyLogoUrl: string + date: string + jobRole: string + interviewType: InterviewType +} + +export type QnaCardItemModel = { + resultStatus: InterviewSearchFilterInterviewResultStatusItem + date: string + company: string + jobRole: string + interviewType: InterviewType + question: string + answer: string +} + +export function mapFrequentCategory(item: FrequentQnaSetCategoryResponse): FrequentQuestionCategory { + return { + categoryId: item.categoryId ?? 0, + categoryName: item.categoryName ?? '', + frequentCount: item.frequentCount ?? 0, + } +} + +export function mapFrequentQuestion(item: FrequentQnaSetCategoryQuestionResponse): QuestionCardModel { + return { + question: item.question ?? '', + company: item.interviewInfo?.companyInfo?.companyName ?? '', + companyLogoUrl: item.interviewInfo?.companyInfo?.companyLogoUrl ?? '', + date: item.interviewInfo?.updatedAt ?? '', + jobRole: item.interviewInfo?.jobCategoryName ?? '', + interviewType: item.interviewInfo?.interviewType ?? 'FIRST', + } +} + +export function mapSearchQuestionToQnaCard(item: QnaSetSearchResponse): QnaCardItemModel { + return { + resultStatus: item.interviewInfo?.interviewResultStatus ?? 'WAIT', + date: item.interviewInfo?.updatedAt ?? '', + company: item.interviewInfo?.companyName ?? '', + jobRole: item.interviewInfo?.jobCategoryName ?? '', + interviewType: item.interviewInfo?.interviewType ?? 'FIRST', + question: item.qnaSetInfo?.questionText ?? '', + answer: item.qnaSetInfo?.answerText ?? '', + } +} From 64c877ec1a0da4933de1651e1ee090305d9bcae1 Mon Sep 17 00:00:00 2001 From: HIHJH <2170095@ewhain.net> Date: Sun, 15 Feb 2026 14:05:54 +0900 Subject: [PATCH 08/13] =?UTF-8?q?feat:=20=EC=9E=90=EC=A3=BC=20=EB=B0=9B?= =?UTF-8?q?=EC=9D=80=20=EC=A7=88=EB=AC=B8=20=EC=BB=B4=ED=8F=AC=EB=84=8C?= =?UTF-8?q?=ED=8A=B8=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../questions/FrequentQuestionsSection.tsx | 111 ------------------ .../frequent/FrequentQuestionsSection.tsx | 106 +++++++++++++++++ .../frequent/category-list/CategoryList.tsx | 27 +++++ .../frequent/question-card/QuestionCard.tsx | 26 ++++ 4 files changed, 159 insertions(+), 111 deletions(-) delete mode 100644 frontend/src/features/dashboard/my-interviews/components/questions/FrequentQuestionsSection.tsx create mode 100644 frontend/src/features/dashboard/my-interviews/components/questions/frequent/FrequentQuestionsSection.tsx create mode 100644 frontend/src/features/dashboard/my-interviews/components/questions/frequent/category-list/CategoryList.tsx create mode 100644 frontend/src/features/dashboard/my-interviews/components/questions/frequent/question-card/QuestionCard.tsx diff --git a/frontend/src/features/dashboard/my-interviews/components/questions/FrequentQuestionsSection.tsx b/frontend/src/features/dashboard/my-interviews/components/questions/FrequentQuestionsSection.tsx deleted file mode 100644 index b8b77466e..000000000 --- a/frontend/src/features/dashboard/my-interviews/components/questions/FrequentQuestionsSection.tsx +++ /dev/null @@ -1,111 +0,0 @@ -import { useState, useEffect } from 'react' -import { CaretDownIcon } from '@/designs/assets' -import { Button } from '@/designs/components' -import { MOCK_QUESTION_PREVIEWS, MOCK_QUESTION_RANKS } from '../../example' -import type { QuestionRankType, QuestionPreviewType } from '../../example' - -export default function FrequentQuestionsSection() { - const [categories] = useState(MOCK_QUESTION_RANKS) - const [selectedCategoryId, setSelectedCategoryId] = useState( - MOCK_QUESTION_RANKS.length > 0 ? MOCK_QUESTION_RANKS[0].categoryId : null, - ) - const [questions, setQuestions] = useState([]) - const [page, setPage] = useState(1) - const [totalPages, setTotalPages] = useState(1) - - // TODO: API 연동 시 훅 분리 - 선택된 카테고리의 질문 조회 - // 지금은 mock data 기반으로 작업해둠 (변경 예정) - useEffect(() => { - if (selectedCategoryId === null) return - const fetchQuestions = async () => { - const selectedCategory = categories.find((c) => c.categoryId === selectedCategoryId) - if (selectedCategory) { - setQuestions(MOCK_QUESTION_PREVIEWS[selectedCategory.categoryName] ?? []) - setTotalPages(20) - } - } - fetchQuestions() - }, [selectedCategoryId, page, categories]) - - return ( -
-

정윤님이 많이 받은 질문들

-
-
- {categories.map(({ categoryId, categoryName, frequentCount }, idx) => ( -
{ - setSelectedCategoryId(categoryId) - setPage(1) - }} - > -
- {idx + 1} - {categoryName} -
- {frequentCount}회 -
- ))} -
-
-
-

- - '{categories.find((c) => c.categoryId === selectedCategoryId)?.categoryName ?? ''}' - - 에 관한 질문과 답변들 -

-
- - - {page}/{totalPages} - - -
-
-
- {questions.length === 0 ? ( -
질문이 없습니다
- ) : ( - questions.map((card, i) => ) - )} -
-
-
-
- ) -} - -const QuestionCard = ({ card }: { card: QuestionPreviewType }) => { - return ( -
-
- {card.company} - {card.company} - {card.date} 응시 -
-
- {card.jobRole} | {card.interviewType} -
-

{card.question}

-
- ) -} diff --git a/frontend/src/features/dashboard/my-interviews/components/questions/frequent/FrequentQuestionsSection.tsx b/frontend/src/features/dashboard/my-interviews/components/questions/frequent/FrequentQuestionsSection.tsx new file mode 100644 index 000000000..e8929563a --- /dev/null +++ b/frontend/src/features/dashboard/my-interviews/components/questions/frequent/FrequentQuestionsSection.tsx @@ -0,0 +1,106 @@ +import { useMemo } from 'react' +import { useGetMyProfileInfo } from '@/apis' +import { CircleLeftIcon, CircleRightIcon } from '@/designs/assets' +import { Button } from '@/designs/components' +import CategoryList from '@/features/dashboard/my-interviews/components/questions/frequent/category-list/CategoryList' +import QuestionCard from '@/features/dashboard/my-interviews/components/questions/frequent/question-card/QuestionCard' +import { DATA_EMPTY_MESSAGE } from '@/features/dashboard/my-interviews/constants/constants' +import { useFrequentQuestions } from './useFrequentQuestions' + +export default function FrequentQuestionsSection() { + const { + categories, + selectedCategoryId, + setSelectedCategoryId, + selectedCategoryQuestions, + page, + setPage, + totalPages, + } = useFrequentQuestions() + const selectedCategoryName = useMemo( + () => categories.find((category) => category.categoryId === selectedCategoryId)?.categoryName ?? '', + [categories, selectedCategoryId], + ) + + const { data: nickname = '회원' } = useGetMyProfileInfo({ + query: { + select: (response) => response.result?.nickname?.trim() || '회원', + }, + }) + + if (categories.length === 0) { + return ( +
+

{nickname}님이 자주 받은 질문들

+
{DATA_EMPTY_MESSAGE.questions}
+
+ ) + } + + return ( +
+

{nickname}님이 자주 받은 질문들

+
+ +
+ setPage(Math.max(1, page - 1))} + onNext={() => setPage(Math.min(totalPages, page + 1))} + /> +
+ {selectedCategoryQuestions.length === 0 ? ( +
{DATA_EMPTY_MESSAGE.questions}
+ ) : ( + selectedCategoryQuestions.map((card, index) => ( + + )) + )} +
+
+
+
+ ) +} + +type FrequentQuestionResultHeaderProps = { + selectedCategoryName: string + page: number + totalPages: number + onPrev: () => void + onNext: () => void +} + +function FrequentQuestionResultHeader({ + selectedCategoryName, + page, + totalPages, + onPrev, + onNext, +}: FrequentQuestionResultHeaderProps) { + return ( +
+

+ '{selectedCategoryName}'에 관한 질문과 답변들 +

+
+
+ + + {page}/{totalPages} + + +
+
+ ) +} diff --git a/frontend/src/features/dashboard/my-interviews/components/questions/frequent/category-list/CategoryList.tsx b/frontend/src/features/dashboard/my-interviews/components/questions/frequent/category-list/CategoryList.tsx new file mode 100644 index 000000000..92a3475c5 --- /dev/null +++ b/frontend/src/features/dashboard/my-interviews/components/questions/frequent/category-list/CategoryList.tsx @@ -0,0 +1,27 @@ +import type { FrequentQuestionCategory } from '../../mappers' + +type Props = { + categories: FrequentQuestionCategory[] + selectedCategoryId: number | null + onSelect: (categoryId: number) => void +} + +export default function CategoryList({ categories, selectedCategoryId, onSelect }: Props) { + return ( +
+ {categories.map(({ categoryId, categoryName, frequentCount }, idx) => ( +
onSelect(categoryId)} + > +
+ {idx + 1} + {categoryName} +
+ {frequentCount}회 +
+ ))} +
+ ) +} diff --git a/frontend/src/features/dashboard/my-interviews/components/questions/frequent/question-card/QuestionCard.tsx b/frontend/src/features/dashboard/my-interviews/components/questions/frequent/question-card/QuestionCard.tsx new file mode 100644 index 000000000..3c59203cc --- /dev/null +++ b/frontend/src/features/dashboard/my-interviews/components/questions/frequent/question-card/QuestionCard.tsx @@ -0,0 +1,26 @@ +import { memo } from 'react' +import { INTERVIEW_TYPE_LABEL } from '@/constants/interviews' +import { formatDate } from '@/features/_common/utils/date' +import type { QuestionCardModel } from '../../mappers' + +function QuestionCard({ card }: { card: QuestionCardModel }) { + return ( +
+
+ {card.companyLogoUrl ? ( + {card.company} + ) : ( +
+ )} + {card.company} + {formatDate(card.date)} 응시 +
+
+ {card.jobRole} | {INTERVIEW_TYPE_LABEL[card.interviewType]} +
+

{card.question}

+
+ ) +} + +export default memo(QuestionCard) From 78983887bbb64a66e350c003a3d29fb7bbda5573 Mon Sep 17 00:00:00 2001 From: HIHJH <2170095@ewhain.net> Date: Sun, 15 Feb 2026 14:06:12 +0900 Subject: [PATCH 09/13] =?UTF-8?q?feat:=20=EC=9E=90=EC=A3=BC=20=EB=B0=9B?= =?UTF-8?q?=EC=9D=80=20=EC=A7=88=EB=AC=B8=20=EC=B9=B4=ED=85=8C=EA=B3=A0?= =?UTF-8?q?=EB=A6=AC=20API=20=EC=97=B0=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../frequent/useFrequentQuestions.ts | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 frontend/src/features/dashboard/my-interviews/components/questions/frequent/useFrequentQuestions.ts diff --git a/frontend/src/features/dashboard/my-interviews/components/questions/frequent/useFrequentQuestions.ts b/frontend/src/features/dashboard/my-interviews/components/questions/frequent/useFrequentQuestions.ts new file mode 100644 index 000000000..7ef22b8ba --- /dev/null +++ b/frontend/src/features/dashboard/my-interviews/components/questions/frequent/useFrequentQuestions.ts @@ -0,0 +1,56 @@ +import { useState, useMemo, useCallback } from 'react' +import { + useGetMyFrequentQnaSetCategories, + useGetMyFrequentQnaSetCategoryQuestions, +} from '@/apis/generated/qna-set-my-controller/qna-set-my-controller' +import { mapFrequentCategory, mapFrequentQuestion } from '../mappers' + +export function useFrequentQuestions(pageSize = 3) { + const [page, setPage] = useState(1) + const { data: categories = [] } = useGetMyFrequentQnaSetCategories( + { page: 0, size: 20 }, + { + query: { + select: (response) => (response.result?.content ?? []).map(mapFrequentCategory), + }, + }, + ) + + const [selectedCategoryId, setSelectedCategoryId] = useState(null) + // 선택 전이면 첫 카테고리를 기본값으로 사용 + const resolvedCategoryId = selectedCategoryId ?? categories[0]?.categoryId ?? null + + const { data: questionsData } = useGetMyFrequentQnaSetCategoryQuestions( + resolvedCategoryId ?? 0, + { + page: Math.max(0, page - 1), + size: pageSize, + }, + { + query: { + enabled: resolvedCategoryId !== null, + select: (response) => ({ + questions: (response.result?.content ?? []).map(mapFrequentQuestion), + totalPages: response.result?.totalPages ?? 1, + }), + }, + }, + ) + + const totalPages = useMemo(() => questionsData?.totalPages ?? 1, [questionsData?.totalPages]) + + const handleCategoryClick = useCallback((categoryId: number) => { + setSelectedCategoryId(categoryId) + setPage(1) + }, []) + + return { + categories, + selectedCategoryId: resolvedCategoryId, + setSelectedCategoryId: handleCategoryClick, + selectedCategoryQuestions: questionsData?.questions ?? [], + page, + setPage, + totalPages, + } +} From 9899079ac9fa03874deda3316e2bed5a94f9bb27 Mon Sep 17 00:00:00 2001 From: HIHJH <2170095@ewhain.net> Date: Sun, 15 Feb 2026 14:07:26 +0900 Subject: [PATCH 10/13] =?UTF-8?q?feat:=20=EB=B0=9B=EC=9D=80=20=EC=A0=84?= =?UTF-8?q?=EC=B2=B4=20=EC=A7=88=EB=AC=B8=20=EC=A1=B0=ED=9A=8C=20=ED=95=84?= =?UTF-8?q?=ED=84=B0=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../list/filter/QuestionFilterControls.tsx | 188 ++++++++++++++++++ 1 file changed, 188 insertions(+) create mode 100644 frontend/src/features/dashboard/my-interviews/components/questions/list/filter/QuestionFilterControls.tsx diff --git a/frontend/src/features/dashboard/my-interviews/components/questions/list/filter/QuestionFilterControls.tsx b/frontend/src/features/dashboard/my-interviews/components/questions/list/filter/QuestionFilterControls.tsx new file mode 100644 index 000000000..b9864b2fc --- /dev/null +++ b/frontend/src/features/dashboard/my-interviews/components/questions/list/filter/QuestionFilterControls.tsx @@ -0,0 +1,188 @@ +import { useMemo, useState } from 'react' +import { CaretDownIcon, FilterIcon } from '@/designs/assets' +import { Button, Checkbox, Modal, PlainCombobox } from '@/designs/components' +import { + EMPTY_QUESTION_FILTER, + QUESTION_SORT_OPTIONS, + STAR_LEVEL_OPTIONS, +} from '@/features/dashboard/my-interviews/constants/constants' +import type { QuestionFilter, StarLevel } from '@/types/interview' + +type Props = { + filter: QuestionFilter + onChange: (filter: QuestionFilter) => void +} + +// TODO: UI 수정 +export default function QuestionFilterControls({ filter, onChange }: Props) { + const [open, setOpen] = useState(false) + const [modalVersion, setModalVersion] = useState(0) + + const selectedCount = useMemo(() => { + const hasStar = filter.hasStarAnalysis === null ? 0 : 1 + return ( + hasStar + + filter.sInclusionLevels.length + + filter.tInclusionLevels.length + + filter.aInclusionLevels.length + + filter.rInclusionLevels.length + ) + }, [filter]) + + const hasFilter = selectedCount > 0 + + return ( + <> +
+ + onChange({ ...filter, sort })} + trigger={ + + } + /> +
+ setOpen(false)} + onApply={(next) => { + onChange(next) + setOpen(false) + }} + /> + + ) +} + +type ModalProps = { + open: boolean + filter: QuestionFilter + onClose: () => void + onApply: (next: QuestionFilter) => void +} + +function QuestionFilterModal({ open, filter, onClose, onApply }: ModalProps) { + const [draft, setDraft] = useState(filter) + + const toggleLevel = ( + key: 'sInclusionLevels' | 'tInclusionLevels' | 'aInclusionLevels' | 'rInclusionLevels', + value: StarLevel, + ) => { + setDraft((prev) => { + const list = prev[key] as string[] + const next = list.includes(value) ? list.filter((item) => item !== value) : [...list, value] + return { ...prev, [key]: next } as QuestionFilter + }) + } + + return ( + +
+
+ STAR 분석 여부 +
+ + + +
+
+ + toggleLevel('sInclusionLevels', value)} + /> + toggleLevel('tInclusionLevels', value)} + /> + toggleLevel('aInclusionLevels', value)} + /> + toggleLevel('rInclusionLevels', value)} + /> + +
+ + +
+
+
+ ) +} + +function LevelGroup({ + label, + selected, + onToggle, +}: { + label: string + selected: string[] + onToggle: (value: StarLevel) => void +}) { + return ( +
+ {label} +
+ {STAR_LEVEL_OPTIONS.map((option) => ( + onToggle(option.value)} + label={option.label} + /> + ))} +
+
+ ) +} From 1d54ef5366ec020dc6c12e07aeae88a181f28150 Mon Sep 17 00:00:00 2001 From: HIHJH <2170095@ewhain.net> Date: Sun, 15 Feb 2026 14:07:59 +0900 Subject: [PATCH 11/13] =?UTF-8?q?feat:=20=EC=A0=84=EC=B2=B4=20=EC=A7=88?= =?UTF-8?q?=EB=AC=B8=20=EC=A1=B0=ED=9A=8C=20useInfiniteQuery=20=EC=82=AC?= =?UTF-8?q?=EC=9A=A9=ED=95=98=EC=97=AC=20API=20=EC=97=B0=EA=B2=B0=20?= =?UTF-8?q?=EB=B0=8F=20=EB=AC=B4=ED=95=9C=EC=8A=A4=ED=81=AC=EB=A1=A4=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../questions/list/useInfiniteQuestionList.ts | 91 +++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 frontend/src/features/dashboard/my-interviews/components/questions/list/useInfiniteQuestionList.ts diff --git a/frontend/src/features/dashboard/my-interviews/components/questions/list/useInfiniteQuestionList.ts b/frontend/src/features/dashboard/my-interviews/components/questions/list/useInfiniteQuestionList.ts new file mode 100644 index 000000000..087d66625 --- /dev/null +++ b/frontend/src/features/dashboard/my-interviews/components/questions/list/useInfiniteQuestionList.ts @@ -0,0 +1,91 @@ +import { useEffect, useMemo, useRef } from 'react' +import { useInfiniteQuery } from '@tanstack/react-query' +import { searchMyQnaSet } from '@/apis/generated/qna-set-my-controller/qna-set-my-controller' +import type { QuestionFilter } from '@/types/interview' +import { mapSearchQuestionToQnaCard, type QnaCardItemModel } from '../mappers' + +const PAGE_SIZE = 4 +const OBSERVER_ROOT_MARGIN = '200px' + +export function useInfiniteQuestionList(filter: QuestionFilter) { + // 화면 하단 감지용 sentinel 요소를 관찰해서 하단 도달 시 다음 페이지 로드 + const loadMoreRef = useRef(null) + + const hasFilterCondition = useMemo(() => hasActiveFilterCondition(filter), [filter]) + + const searchBody = useMemo(() => toQuestionSearchRequestBody(filter), [filter]) + const sortParam = useMemo(() => (filter.sort ? [filter.sort] : undefined), [filter.sort]) + + const { data, isPending, isFetchingNextPage, hasNextPage, fetchNextPage, isError } = useInfiniteQuery({ + queryKey: ['my-interviews', 'question-list', searchBody, sortParam], + initialPageParam: 0, + queryFn: ({ pageParam }) => + searchMyQnaSet(searchBody, { + page: pageParam, + size: PAGE_SIZE, + sort: sortParam, + }), + getNextPageParam: (lastPage, _allPages, lastPageParam) => { + const totalPages = lastPage.result?.totalPages ?? 0 + const nextPage = lastPageParam + 1 + return nextPage < totalPages ? nextPage : undefined + }, + }) + + useEffect(() => { + const target = loadMoreRef.current + if (!target || !hasNextPage || isPending || isFetchingNextPage) return + + const observer = new IntersectionObserver( + (entries) => { + if (entries[0]?.isIntersecting) void fetchNextPage() + }, + { rootMargin: OBSERVER_ROOT_MARGIN }, + ) + + observer.observe(target) + return () => observer.disconnect() + }, [fetchNextPage, hasNextPage, isFetchingNextPage, isPending]) + + const items = useMemo(() => toFlatQuestionItems(data?.pages), [data?.pages]) + + const emptyMessage = useMemo(() => getEmptyMessage({ isError, hasFilterCondition }), [hasFilterCondition, isError]) + + return { + items, + loadMoreRef, + isInitialLoading: isPending, + isFetchingNext: isFetchingNextPage, + isPending, + hasNextPage: Boolean(hasNextPage), + emptyMessage, + } +} + +const toQuestionSearchRequestBody = (filter: QuestionFilter) => ({ + keyword: filter.keyword || undefined, + searchFilter: { + hasStarAnalysis: filter.hasStarAnalysis ?? undefined, + sInclusionLevels: filter.sInclusionLevels.length > 0 ? filter.sInclusionLevels : undefined, + tInclusionLevels: filter.tInclusionLevels.length > 0 ? filter.tInclusionLevels : undefined, + aInclusionLevels: filter.aInclusionLevels.length > 0 ? filter.aInclusionLevels : undefined, + rInclusionLevels: filter.rInclusionLevels.length > 0 ? filter.rInclusionLevels : undefined, + }, +}) + +const hasActiveFilterCondition = (filter: QuestionFilter) => + filter.keyword.trim().length > 0 || + filter.hasStarAnalysis !== null || + filter.sInclusionLevels.length > 0 || + filter.tInclusionLevels.length > 0 || + filter.aInclusionLevels.length > 0 || + filter.rInclusionLevels.length > 0 + +const toFlatQuestionItems = (pages: Array<{ result?: { content?: unknown[] } }> | undefined): QnaCardItemModel[] => + (pages ?? []).flatMap((page) => (page.result?.content ?? []).map((item) => mapSearchQuestionToQnaCard(item as never))) + +const getEmptyMessage = ({ isError, hasFilterCondition }: { isError: boolean; hasFilterCondition: boolean }) => { + if (isError) return '질문 목록을 불러오지 못했어요. 잠시 후 다시 시도해 주세요.' + if (hasFilterCondition) return '선택한 검색/필터 조건에 맞는 질문이 없어요. 조건을 바꿔서 다시 확인해보세요.' + return '질문 데이터가 없어요. 면접 기록을 진행해서 질문을 모아보세요.' +} From d6f22003c2cc7c50a41f093aa26c099227b93416 Mon Sep 17 00:00:00 2001 From: HIHJH <2170095@ewhain.net> Date: Sun, 15 Feb 2026 14:08:29 +0900 Subject: [PATCH 12/13] =?UTF-8?q?feat:=20=EC=A0=84=EC=B2=B4=20=EC=A7=88?= =?UTF-8?q?=EB=AC=B8=20=EC=A1=B0=ED=9A=8C=20=EC=98=81=EC=97=AD=20=EC=BB=B4?= =?UTF-8?q?=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EB=B6=84=EB=A6=AC=20=EB=B0=8F=20?= =?UTF-8?q?=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../questions/QuestionListSection.tsx | 21 ---------- .../components/questions/index.ts | 6 +-- .../questions/list/QuestionListSection.tsx | 39 +++++++++++++++++++ .../questions/{ => list/qna-card}/QnaCard.tsx | 6 ++- .../components/FilterResultList.tsx | 2 +- 5 files changed, 47 insertions(+), 27 deletions(-) delete mode 100644 frontend/src/features/dashboard/my-interviews/components/questions/QuestionListSection.tsx create mode 100644 frontend/src/features/dashboard/my-interviews/components/questions/list/QuestionListSection.tsx rename frontend/src/features/dashboard/my-interviews/components/questions/{ => list/qna-card}/QnaCard.tsx (75%) diff --git a/frontend/src/features/dashboard/my-interviews/components/questions/QuestionListSection.tsx b/frontend/src/features/dashboard/my-interviews/components/questions/QuestionListSection.tsx deleted file mode 100644 index 4c9abf72f..000000000 --- a/frontend/src/features/dashboard/my-interviews/components/questions/QuestionListSection.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import type { InterviewFilter } from '@/types/interview' -import { MOCK_QNA } from '../../example' -import QnaCard from './QnaCard' - -type QuestionListSectionProps = { - filter: InterviewFilter -} - -export default function QuestionListSection({ filter }: QuestionListSectionProps) { - const filteredQuestions = MOCK_QNA.filter((item) => !filter.keyword || item.company.includes(filter.keyword)) - - return ( -
-
- {filteredQuestions.map((item, i) => ( - - ))} -
-
- ) -} diff --git a/frontend/src/features/dashboard/my-interviews/components/questions/index.ts b/frontend/src/features/dashboard/my-interviews/components/questions/index.ts index 2f540d813..3badbeab0 100644 --- a/frontend/src/features/dashboard/my-interviews/components/questions/index.ts +++ b/frontend/src/features/dashboard/my-interviews/components/questions/index.ts @@ -1,3 +1,3 @@ -export { default as FrequentQuestionsSection } from './FrequentQuestionsSection' -export { default as QnaCard } from './QnaCard' -export { default as QuestionListSection } from './QuestionListSection' +export { default as FrequentQuestionsSection } from './frequent/FrequentQuestionsSection' +export { default as QnaCard } from './list/qna-card/QnaCard' +export { default as QuestionListSection } from './list/QuestionListSection' diff --git a/frontend/src/features/dashboard/my-interviews/components/questions/list/QuestionListSection.tsx b/frontend/src/features/dashboard/my-interviews/components/questions/list/QuestionListSection.tsx new file mode 100644 index 000000000..a69e5b3b4 --- /dev/null +++ b/frontend/src/features/dashboard/my-interviews/components/questions/list/QuestionListSection.tsx @@ -0,0 +1,39 @@ +import QnaCard from '@/features/dashboard/my-interviews/components/questions/list/qna-card/QnaCard' +import type { QuestionFilter } from '@/types/interview' +import { useInfiniteQuestionList } from './useInfiniteQuestionList' + +type QuestionListSectionProps = { + filter: QuestionFilter +} + +export default function QuestionListSection({ filter }: QuestionListSectionProps) { + const { items, loadMoreRef, isInitialLoading, isFetchingNext, isPending, emptyMessage } = + useInfiniteQuestionList(filter) + const hasItems = items.length > 0 + const isLoadingMore = hasItems && (isFetchingNext || isPending) + + const renderListContent = () => { + if (isInitialLoading) { + return + } + if (!hasItems) { + return + } + return items.map((item, i) => ) + } + + return ( +
+
{renderListContent()}
+ {hasItems && ( +
+ {isLoadingMore ? '불러오는 중...' : ''} +
+ )} +
+ ) +} + +function StatusText({ message }: { message: string }) { + return
{message}
+} diff --git a/frontend/src/features/dashboard/my-interviews/components/questions/QnaCard.tsx b/frontend/src/features/dashboard/my-interviews/components/questions/list/qna-card/QnaCard.tsx similarity index 75% rename from frontend/src/features/dashboard/my-interviews/components/questions/QnaCard.tsx rename to frontend/src/features/dashboard/my-interviews/components/questions/list/qna-card/QnaCard.tsx index 48d829dc7..95f7d29ff 100644 --- a/frontend/src/features/dashboard/my-interviews/components/questions/QnaCard.tsx +++ b/frontend/src/features/dashboard/my-interviews/components/questions/list/qna-card/QnaCard.tsx @@ -1,8 +1,9 @@ import InterviewCard from '@/features/dashboard/my-interviews/components/interviews/InterviewCard' +import type { InterviewResultStatus } from '@/features/dashboard/my-interviews/constants/constants' import type { InterviewType } from '@/types/interview' type QnaCardProps = { - resultStatus: 'wait' | 'pass' | 'fail' + resultStatus: InterviewResultStatus date: string company: string jobRole: string @@ -11,10 +12,11 @@ type QnaCardProps = { answer: string } +// TODO: 클릭하면 retro details 모달 열리도록 수정 export default function QnaCard({ question, answer, ...cardProps }: QnaCardProps) { return ( -
+
Q.

{question}

diff --git a/frontend/src/features/dashboard/trend-questions/components/FilterResultList.tsx b/frontend/src/features/dashboard/trend-questions/components/FilterResultList.tsx index 6c49d555d..6ae5be5e2 100644 --- a/frontend/src/features/dashboard/trend-questions/components/FilterResultList.tsx +++ b/frontend/src/features/dashboard/trend-questions/components/FilterResultList.tsx @@ -1,4 +1,4 @@ -import QnaCard from '@/features/dashboard/my-interviews/components/questions/QnaCard' +import QnaCard from '@/features/dashboard/my-interviews/components/questions/list/qna-card/QnaCard' import { MOCK_QNA } from '@/features/dashboard/my-interviews/example' import FilterBadges from '@/features/dashboard/trend-questions/components/FilterBadges' import type { IndustryJobFilterState } from '@/features/dashboard/trend-questions/hooks/useIndustryJobFilter' From b28f2ee163144dd4ffb0690b2105be6e78c7b28b Mon Sep 17 00:00:00 2001 From: HIHJH <2170095@ewhain.net> Date: Sun, 15 Feb 2026 15:43:40 +0900 Subject: [PATCH 13/13] =?UTF-8?q?fix:=20=ED=83=80=EC=9E=85=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/questions/list/useInfiniteQuestionList.ts | 5 +++-- frontend/src/types/interview.d.ts | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/frontend/src/features/dashboard/my-interviews/components/questions/list/useInfiniteQuestionList.ts b/frontend/src/features/dashboard/my-interviews/components/questions/list/useInfiniteQuestionList.ts index 087d66625..5b880f30f 100644 --- a/frontend/src/features/dashboard/my-interviews/components/questions/list/useInfiniteQuestionList.ts +++ b/frontend/src/features/dashboard/my-interviews/components/questions/list/useInfiniteQuestionList.ts @@ -1,6 +1,7 @@ import { useEffect, useMemo, useRef } from 'react' import { useInfiniteQuery } from '@tanstack/react-query' import { searchMyQnaSet } from '@/apis/generated/qna-set-my-controller/qna-set-my-controller' +import type { ApiResponsePageQnaSetSearchResponse } from '@/apis/generated/refit-api.schemas' import type { QuestionFilter } from '@/types/interview' import { mapSearchQuestionToQnaCard, type QnaCardItemModel } from '../mappers' @@ -81,8 +82,8 @@ const hasActiveFilterCondition = (filter: QuestionFilter) => filter.aInclusionLevels.length > 0 || filter.rInclusionLevels.length > 0 -const toFlatQuestionItems = (pages: Array<{ result?: { content?: unknown[] } }> | undefined): QnaCardItemModel[] => - (pages ?? []).flatMap((page) => (page.result?.content ?? []).map((item) => mapSearchQuestionToQnaCard(item as never))) +const toFlatQuestionItems = (pages: ApiResponsePageQnaSetSearchResponse[] | undefined): QnaCardItemModel[] => + (pages ?? []).flatMap((page) => (page.result?.content ?? []).map((item) => mapSearchQuestionToQnaCard(item))) const getEmptyMessage = ({ isError, hasFilterCondition }: { isError: boolean; hasFilterCondition: boolean }) => { if (isError) return '질문 목록을 불러오지 못했어요. 잠시 후 다시 시도해 주세요.' diff --git a/frontend/src/types/interview.d.ts b/frontend/src/types/interview.d.ts index 853a34724..54092a06c 100644 --- a/frontend/src/types/interview.d.ts +++ b/frontend/src/types/interview.d.ts @@ -80,7 +80,7 @@ export type StarLevel = type ApiQuestionSearchFilter = NonNullable export type QuestionFilter = { - keyword: QnaSetSearchRequest['keyword'] extends string | undefined ? string : never + keyword: string sort: string hasStarAnalysis: ApiQuestionSearchFilter['hasStarAnalysis'] | null sInclusionLevels: NonNullable