From e1ac8e43b570f3d740e9ea0c0dabda687db33840 Mon Sep 17 00:00:00 2001 From: JW_Ahn0 Date: Mon, 23 Feb 2026 14:36:27 +0900 Subject: [PATCH 1/5] =?UTF-8?q?feat:=20=ED=95=99=EA=B5=90=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20=EB=B0=8F=20=EC=A1=B0=ED=9A=8C=20=EA=B2=B0=EA=B3=BC?= =?UTF-8?q?=20=ED=99=94=EB=A9=B4=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 학교 조회: /univ/search, /school/search 키워드 검색, 디바운스, 드롭다운 선택 시 재검색 방지 - 결과보기: /univ, /school 가능 여부 API 호출, data에 '가능' 포함 시 성공 처리 - schoolSearchResultStore 추가, eligibilityConstants 학교 조회/결과 문구 추가 --- app/eligibility/school/page.tsx | 35 +++++ app/eligibility/school/result/page.tsx | 20 +++ .../eligibility/shcoolSearchEmptyImg.tsx | 115 +++++++++++++++ .../eligibility/shcoolSearchFillImg.tsx | 139 ++++++++++++++++++ .../eligibility/shcoolSearchEmptyImg.svg | 38 +++++ .../eligibility/shcoolSearchFillImg.svg | 46 ++++++ .../eligibility/api/schoolUnivSearchApi.ts | 49 ++++++ .../eligibility/hooks/useSchoolSearch.ts | 113 ++++++++++++++ .../hooks/useSchoolSearchResult.ts | 24 +++ .../eligibility/model/eligibilityConstants.ts | 18 +++ .../model/eligibilityDecisionTree.ts | 2 + .../model/schoolSearchResultStore.ts | 15 ++ .../common/eligibilityComponentRenderer.tsx | 2 + .../ui/school/SchoolResultContentSection.tsx | 57 +++++++ .../ui/school/SchoolResultHeader.tsx | 12 ++ .../ui/school/SchoolResultTryAgainSection.tsx | 24 +++ .../ui/school/SchoolSearchFormSection.tsx | 40 +++++ .../ui/school/SchoolSearchHeader.tsx | 11 ++ .../SchoolSearchResultButtonSection.tsx | 29 ++++ src/features/eligibility/ui/school/index.ts | 6 + .../ui/searchBarLabel/searchBarLabel.tsx | 18 ++- 21 files changed, 811 insertions(+), 2 deletions(-) create mode 100644 app/eligibility/school/page.tsx create mode 100644 app/eligibility/school/result/page.tsx create mode 100644 src/assets/images/eligibility/shcoolSearchEmptyImg.tsx create mode 100644 src/assets/images/eligibility/shcoolSearchFillImg.tsx create mode 100644 src/assets/images/svgFile/eligibility/shcoolSearchEmptyImg.svg create mode 100644 src/assets/images/svgFile/eligibility/shcoolSearchFillImg.svg create mode 100644 src/features/eligibility/api/schoolUnivSearchApi.ts create mode 100644 src/features/eligibility/hooks/useSchoolSearch.ts create mode 100644 src/features/eligibility/hooks/useSchoolSearchResult.ts create mode 100644 src/features/eligibility/model/schoolSearchResultStore.ts create mode 100644 src/features/eligibility/ui/school/SchoolResultContentSection.tsx create mode 100644 src/features/eligibility/ui/school/SchoolResultHeader.tsx create mode 100644 src/features/eligibility/ui/school/SchoolResultTryAgainSection.tsx create mode 100644 src/features/eligibility/ui/school/SchoolSearchFormSection.tsx create mode 100644 src/features/eligibility/ui/school/SchoolSearchHeader.tsx create mode 100644 src/features/eligibility/ui/school/SchoolSearchResultButtonSection.tsx create mode 100644 src/features/eligibility/ui/school/index.ts diff --git a/app/eligibility/school/page.tsx b/app/eligibility/school/page.tsx new file mode 100644 index 0000000..c7e4bcb --- /dev/null +++ b/app/eligibility/school/page.tsx @@ -0,0 +1,35 @@ +"use client"; + +import { useSchoolSearch } from "@/src/features/eligibility/hooks/useSchoolSearch"; +import { + SchoolSearchHeader, + SchoolSearchFormSection, + SchoolSearchResultButtonSection, +} from "@/src/features/eligibility/ui/school"; + +export default function SchoolSearchPage() { + const { + keyword, + setKeyword, + options, + hasKeyword, + handleSelect, + handleResultClick, + } = useSchoolSearch(); + + return ( +
+ + + +
+ ); +} diff --git a/app/eligibility/school/result/page.tsx b/app/eligibility/school/result/page.tsx new file mode 100644 index 0000000..653c6fa --- /dev/null +++ b/app/eligibility/school/result/page.tsx @@ -0,0 +1,20 @@ +"use client"; + +import { useSchoolSearchResult } from "@/src/features/eligibility/hooks/useSchoolSearchResult"; +import { + SchoolResultHeader, + SchoolResultContentSection, + SchoolResultTryAgainSection, +} from "@/src/features/eligibility/ui/school"; + +export default function SchoolResultPage() { + const { keyword, message, isEligible, handleTryAgain } = useSchoolSearchResult(); + + return ( +
+ + + +
+ ); +} diff --git a/src/assets/images/eligibility/shcoolSearchEmptyImg.tsx b/src/assets/images/eligibility/shcoolSearchEmptyImg.tsx new file mode 100644 index 0000000..fb8564d --- /dev/null +++ b/src/assets/images/eligibility/shcoolSearchEmptyImg.tsx @@ -0,0 +1,115 @@ +const SchoolSearchEmptyImg = () => { + return ( + + + + + + + + + + + + + + + + + + + + +
+
+ + + +
+ + + + + + + + + + + + + + + + +
+ ); +}; + +export default SchoolSearchEmptyImg; diff --git a/src/assets/images/eligibility/shcoolSearchFillImg.tsx b/src/assets/images/eligibility/shcoolSearchFillImg.tsx new file mode 100644 index 0000000..a92352d --- /dev/null +++ b/src/assets/images/eligibility/shcoolSearchFillImg.tsx @@ -0,0 +1,139 @@ +const SchoolSearchFillImg = () => { + return ( + + + + + + + + + + + + + + + + + + + + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + +
+ ); +}; + +export default SchoolSearchFillImg; diff --git a/src/assets/images/svgFile/eligibility/shcoolSearchEmptyImg.svg b/src/assets/images/svgFile/eligibility/shcoolSearchEmptyImg.svg new file mode 100644 index 0000000..b79ad11 --- /dev/null +++ b/src/assets/images/svgFile/eligibility/shcoolSearchEmptyImg.svg @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + + + + + + + + + +
diff --git a/src/assets/images/svgFile/eligibility/shcoolSearchFillImg.svg b/src/assets/images/svgFile/eligibility/shcoolSearchFillImg.svg new file mode 100644 index 0000000..856c989 --- /dev/null +++ b/src/assets/images/svgFile/eligibility/shcoolSearchFillImg.svg @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + +
diff --git a/src/features/eligibility/api/schoolUnivSearchApi.ts b/src/features/eligibility/api/schoolUnivSearchApi.ts new file mode 100644 index 0000000..a4ca625 --- /dev/null +++ b/src/features/eligibility/api/schoolUnivSearchApi.ts @@ -0,0 +1,49 @@ +import { http } from "@/src/shared/api/http"; +import { + UNIV_SEARCH_ENDPOINT, + SCHOOL_SEARCH_ENDPOINT, + UNIV_AVAILABILITY_ENDPOINT, + SCHOOL_AVAILABILITY_ENDPOINT, +} from "@/src/shared/api/endpoints"; +import type { IResponse } from "@/src/shared/types/response"; + +/** /univ/search, /school/search 응답 항목 */ +export interface SchoolUnivSearchItem { + id: string; + name: string; + campusType: string; + collegeType: string; +} + +type SearchResponse = IResponse; + +/** GET /univ/search - 대학교 검색 */ +export async function getUnivSearch(keyword: string): Promise { + return http.get(UNIV_SEARCH_ENDPOINT, { + keyword: keyword.trim() || undefined, + }); +} + +/** GET /school/search - 고등학교 검색 */ +export async function getSchoolSearch(keyword: string): Promise { + return http.get(SCHOOL_SEARCH_ENDPOINT, { + keyword: keyword.trim() || undefined, + }); +} + +/** 가능여부 응답: data에 안내 문구 문자열 */ +type AvailabilityResponse = IResponse; + +/** GET /univ - 대학교 공공임대주택 지원 가능 여부 */ +export async function getUnivAvailability(keyword: string): Promise { + return http.get(UNIV_AVAILABILITY_ENDPOINT, { + keyword, + }); +} + +/** GET /school - 고등학교 공공임대주택 지원 가능 여부 */ +export async function getSchoolAvailability(keyword: string): Promise { + return http.get(SCHOOL_AVAILABILITY_ENDPOINT, { + keyword, + }); +} diff --git a/src/features/eligibility/hooks/useSchoolSearch.ts b/src/features/eligibility/hooks/useSchoolSearch.ts new file mode 100644 index 0000000..6424c39 --- /dev/null +++ b/src/features/eligibility/hooks/useSchoolSearch.ts @@ -0,0 +1,113 @@ +import { useCallback, useEffect, useRef, useState } from "react"; +import { useRouter } from "next/navigation"; +import { useDebounce } from "@/src/shared/hooks/useDebounce/useDebounce"; +import type { SearchBarOption } from "@/src/shared/ui/searchBar/type"; +import { + getUnivSearch, + getSchoolSearch, + getUnivAvailability, + getSchoolAvailability, +} from "@/src/features/eligibility/api/schoolUnivSearchApi"; +import { useSchoolSearchResultStore } from "@/src/features/eligibility/model/schoolSearchResultStore"; + +function toSearchBarOption(item: { + id: string; + name: string; + collegeType: string; +}): SearchBarOption { + return { + key: item.id, + value: item.name, + description: item.collegeType, + }; +} + +const DEBOUNCE_MS = 300; + +const RESULT_PATH = "/eligibility/school/result"; + +export function useSchoolSearch() { + const router = useRouter(); + const setResult = useSchoolSearchResultStore(s => s.setResult); + const [keyword, setKeywordState] = useState(""); + const [selectedSchoolId, setSelectedSchoolId] = useState(null); + const [options, setOptions] = useState([]); + const debouncedKeyword = useDebounce(keyword, DEBOUNCE_MS); + const hasKeyword = keyword.trim().length > 0; + const skipNextSearchRef = useRef(false); + + const setKeyword = useCallback((value: string) => { + setKeywordState(value); + setSelectedSchoolId(null); + }, []); + + useEffect(() => { + const q = debouncedKeyword.trim(); + if (!q) { + setOptions([]); + return; + } + if (skipNextSearchRef.current) { + skipNextSearchRef.current = false; + setOptions([]); + return; + } + let cancelled = false; + (async () => { + try { + const univRes = await getUnivSearch(q); + if (cancelled) return; + const schoolRes = await getSchoolSearch(q); + if (cancelled) return; + const univList = univRes.data ?? []; + const schoolList = schoolRes.data ?? []; + const merged = [...univList.map(toSearchBarOption), ...schoolList.map(toSearchBarOption)]; + setOptions(merged); + } catch { + if (!cancelled) setOptions([]); + } + })(); + return () => { + cancelled = true; + }; + }, [debouncedKeyword]); + + const handleSelect = (option: SearchBarOption) => { + skipNextSearchRef.current = true; + setKeywordState(option.value); + setSelectedSchoolId(option.key); + setOptions([]); + }; + + const handleResultClick = useCallback(async () => { + if (!hasKeyword) return; + const q = keyword.trim(); + if (!q) return; + const ELIGIBLE_KEYWORD = "가능"; + try { + const [univRes, schoolRes] = await Promise.all([ + getUnivAvailability(q), + getSchoolAvailability(q), + ]); + const univMsg = typeof univRes.data === "string" ? univRes.data : null; + const schoolMsg = typeof schoolRes.data === "string" ? schoolRes.data : null; + const successMessage = [univMsg, schoolMsg].find(m => m?.includes(ELIGIBLE_KEYWORD)) ?? null; + const message = successMessage ?? univMsg ?? schoolMsg ?? null; + setResult(keyword.trim(), message); + router.push(RESULT_PATH); + } catch { + setResult(keyword.trim(), null); + router.push(RESULT_PATH); + } + }, [hasKeyword, keyword, setResult, router]); + + return { + keyword, + setKeyword, + options, + hasKeyword, + hasSelection: selectedSchoolId != null, + handleSelect, + handleResultClick, + }; +} diff --git a/src/features/eligibility/hooks/useSchoolSearchResult.ts b/src/features/eligibility/hooks/useSchoolSearchResult.ts new file mode 100644 index 0000000..f41b1c4 --- /dev/null +++ b/src/features/eligibility/hooks/useSchoolSearchResult.ts @@ -0,0 +1,24 @@ +import { useCallback } from "react"; +import { useRouter } from "next/navigation"; +import { useSchoolSearchResultStore } from "@/src/features/eligibility/model/schoolSearchResultStore"; + +const ELIGIBLE_KEYWORD = "가능"; +const SCHOOL_SEARCH_PATH = "/eligibility/school"; + +export function useSchoolSearchResult() { + const router = useRouter(); + const { keyword, message, clear } = useSchoolSearchResultStore(); + const isEligible = message != null && message.includes(ELIGIBLE_KEYWORD); + + const handleTryAgain = useCallback(() => { + clear(); + router.push(SCHOOL_SEARCH_PATH); + }, [clear, router]); + + return { + keyword, + message, + isEligible, + handleTryAgain, + }; +} diff --git a/src/features/eligibility/model/eligibilityConstants.ts b/src/features/eligibility/model/eligibilityConstants.ts index 002fcc3..5149062 100644 --- a/src/features/eligibility/model/eligibilityConstants.ts +++ b/src/features/eligibility/model/eligibilityConstants.ts @@ -10,3 +10,21 @@ export const ELIGIBILITY_RESULT_BANNER_TITLE = (userName: string) => export const ELIGIBILITY_RESULT_BANNER_SUBTITLE = "입력하신 정보가 맞는지 확인해 보세요"; export const ELIGIBILITY_RESULT_BUTTON = "결과보기"; export const ELIGIBILITY_COMPONENT_RENDERER_PAGE_TITLE = "자격 진단"; + +/** 학교 조회 화면 문구 */ +export const SCHOOL_SEARCH_TITLE = "학교 조회"; +export const SCHOOL_SEARCH_INTRO = + "우리 학교가 공공임대주택 지원자격 대상에 해당하는지 검색해 보세요!"; +export const SCHOOL_SEARCH_LABEL = "학교명을 검색해 주세요 (고등학교/대학교)"; +export const SCHOOL_SEARCH_PLACEHOLDER = "예) 핀하우스 대학교"; +export const SCHOOL_RESULT_BUTTON = "결과보기"; + +/** 학교 조회 결과 화면 문구 */ +export const SCHOOL_RESULT_PAGE_TITLE = "학교 조회 결과"; +export const SCHOOL_RESULT_EMPTY_LINE1 = (keyword: string) => `${keyword}는`; +export const SCHOOL_RESULT_EMPTY_LINE2 = "조회되지 않아요"; +export const SCHOOL_RESULT_EMPTY_HINT1 = "학교 이름을 다시 확인해 주세요."; +export const SCHOOL_RESULT_EMPTY_HINT2 = + "목록에 없는 학교는 공공임대주택 인정 학교가 아닐 수 있어요."; +export const SCHOOL_RESULT_TRY_AGAIN_BUTTON = "다시하기"; +export const SCHOOL_RESULT_FILL_HINT = "지원 가능한 지역 범위는 공고를 확인하세요"; diff --git a/src/features/eligibility/model/eligibilityDecisionTree.ts b/src/features/eligibility/model/eligibilityDecisionTree.ts index f66cebb..51d665b 100644 --- a/src/features/eligibility/model/eligibilityDecisionTree.ts +++ b/src/features/eligibility/model/eligibilityDecisionTree.ts @@ -735,6 +735,8 @@ export const eligibilityDecisionTree: StepConfig[] = [ title: "우리 학교가 공공 임대주택에서 인정되는 학교 기준에 해당되는지 궁금하다면?", description: "학점은행제, 사내대학, 원격대학, 재외국민은 제외합니다\n대안학교, 검정고시는 포함하지만 해외 고등학교 졸업은 제외합니다", + action: "path", + path: "/eligibility/school", }, }, ], diff --git a/src/features/eligibility/model/schoolSearchResultStore.ts b/src/features/eligibility/model/schoolSearchResultStore.ts new file mode 100644 index 0000000..cdf8fab --- /dev/null +++ b/src/features/eligibility/model/schoolSearchResultStore.ts @@ -0,0 +1,15 @@ +import { create } from "zustand"; + +export type SchoolSearchResultState = { + keyword: string; + message: string | null; + setResult: (keyword: string, message: string | null) => void; + clear: () => void; +}; + +export const useSchoolSearchResultStore = create(set => ({ + keyword: "", + message: null, + setResult: (keyword, message) => set({ keyword, message }), + clear: () => set({ keyword: "", message: null }), +})); diff --git a/src/features/eligibility/ui/common/eligibilityComponentRenderer.tsx b/src/features/eligibility/ui/common/eligibilityComponentRenderer.tsx index ed5d278..88bee2a 100644 --- a/src/features/eligibility/ui/common/eligibilityComponentRenderer.tsx +++ b/src/features/eligibility/ui/common/eligibilityComponentRenderer.tsx @@ -306,6 +306,8 @@ export const EligibilityComponentRenderer = ({ config }: EligibilityComponentRen onClick = () => router.push("/home"); } else if (config.props.action === "back") { onClick = () => router.back(); + } else if (config.props.action === "path") { + onClick = () => router.push(config.props.path); } } diff --git a/src/features/eligibility/ui/school/SchoolResultContentSection.tsx b/src/features/eligibility/ui/school/SchoolResultContentSection.tsx new file mode 100644 index 0000000..5f75094 --- /dev/null +++ b/src/features/eligibility/ui/school/SchoolResultContentSection.tsx @@ -0,0 +1,57 @@ +import SchoolSearchFillImg from "@/src/assets/images/eligibility/shcoolSearchFillImg"; +import SchoolSearchEmptyImg from "@/src/assets/images/eligibility/shcoolSearchEmptyImg"; +import { + SCHOOL_RESULT_EMPTY_LINE1, + SCHOOL_RESULT_EMPTY_LINE2, + SCHOOL_RESULT_EMPTY_HINT1, + SCHOOL_RESULT_EMPTY_HINT2, + SCHOOL_RESULT_FILL_HINT, +} from "@/src/features/eligibility/model/eligibilityConstants"; + +type SchoolResultContentSectionProps = { + isEligible: boolean; + message: string | null; + keyword: string; +}; + +export function SchoolResultContentSection({ + isEligible, + message, + keyword, +}: SchoolResultContentSectionProps) { + return ( +
+
+ {isEligible ? : } +
+ +
+ {isEligible && message ? ( + <> +

+ {message} +

+

+ {SCHOOL_RESULT_FILL_HINT} +

+ + ) : ( + <> +

+ {keyword ? SCHOOL_RESULT_EMPTY_LINE1(keyword) : ""} +

+

+ {SCHOOL_RESULT_EMPTY_LINE2} +

+

+ {SCHOOL_RESULT_EMPTY_HINT1} +

+

+ {SCHOOL_RESULT_EMPTY_HINT2} +

+ + )} +
+
+ ); +} diff --git a/src/features/eligibility/ui/school/SchoolResultHeader.tsx b/src/features/eligibility/ui/school/SchoolResultHeader.tsx new file mode 100644 index 0000000..4d7c35c --- /dev/null +++ b/src/features/eligibility/ui/school/SchoolResultHeader.tsx @@ -0,0 +1,12 @@ +import { DefaultHeader } from "@/src/shared/ui/header"; +import { SCHOOL_RESULT_PAGE_TITLE } from "@/src/features/eligibility/model/eligibilityConstants"; + +const BACK_PATH = "/eligibility/school"; + +export function SchoolResultHeader() { + return ( +
+ +
+ ); +} diff --git a/src/features/eligibility/ui/school/SchoolResultTryAgainSection.tsx b/src/features/eligibility/ui/school/SchoolResultTryAgainSection.tsx new file mode 100644 index 0000000..c51e304 --- /dev/null +++ b/src/features/eligibility/ui/school/SchoolResultTryAgainSection.tsx @@ -0,0 +1,24 @@ +import { Button } from "@/src/shared/lib/headlessUi/button/button"; +import { SCHOOL_RESULT_TRY_AGAIN_BUTTON } from "@/src/features/eligibility/model/eligibilityConstants"; + +type SchoolResultTryAgainSectionProps = { + onTryAgain: () => void; +}; + +export function SchoolResultTryAgainSection({ onTryAgain }: SchoolResultTryAgainSectionProps) { + return ( +
+ +
+ ); +} diff --git a/src/features/eligibility/ui/school/SchoolSearchFormSection.tsx b/src/features/eligibility/ui/school/SchoolSearchFormSection.tsx new file mode 100644 index 0000000..4de79e6 --- /dev/null +++ b/src/features/eligibility/ui/school/SchoolSearchFormSection.tsx @@ -0,0 +1,40 @@ +import { SearchBarLabel } from "@/src/shared/ui/searchBarLabel"; +import type { SearchBarOption } from "@/src/shared/ui/searchBar/type"; +import { + SCHOOL_SEARCH_INTRO, + SCHOOL_SEARCH_LABEL, + SCHOOL_SEARCH_PLACEHOLDER, +} from "@/src/features/eligibility/model/eligibilityConstants"; + +type SchoolSearchFormSectionProps = { + keyword: string; + options: SearchBarOption[]; + onKeywordChange: (value: string) => void; + onSelect: (option: SearchBarOption) => void; +}; + +export function SchoolSearchFormSection({ + keyword, + options, + onKeywordChange, + onSelect, +}: SchoolSearchFormSectionProps) { + return ( +
+

+ {SCHOOL_SEARCH_INTRO} +

+
+ onKeywordChange(e.target.value)} + options={options} + onSelect={onSelect} + /> +
+
+ ); +} diff --git a/src/features/eligibility/ui/school/SchoolSearchHeader.tsx b/src/features/eligibility/ui/school/SchoolSearchHeader.tsx new file mode 100644 index 0000000..1bf657e --- /dev/null +++ b/src/features/eligibility/ui/school/SchoolSearchHeader.tsx @@ -0,0 +1,11 @@ +import { DefaultHeader } from "@/src/shared/ui/header"; +import { SCHOOL_SEARCH_TITLE } from "@/src/features/eligibility/model/eligibilityConstants"; + +const BACK_PATH = "/eligibility?step=youngSingle001"; +export function SchoolSearchHeader() { + return ( +
+ +
+ ); +} diff --git a/src/features/eligibility/ui/school/SchoolSearchResultButtonSection.tsx b/src/features/eligibility/ui/school/SchoolSearchResultButtonSection.tsx new file mode 100644 index 0000000..94d9ebb --- /dev/null +++ b/src/features/eligibility/ui/school/SchoolSearchResultButtonSection.tsx @@ -0,0 +1,29 @@ +import { Button } from "@/src/shared/lib/headlessUi/button/button"; +import { SCHOOL_RESULT_BUTTON } from "@/src/features/eligibility/model/eligibilityConstants"; + +type SchoolSearchResultButtonSectionProps = { + hasKeyword: boolean; + onResultClick: () => void; +}; + +export function SchoolSearchResultButtonSection({ + hasKeyword, + onResultClick, +}: SchoolSearchResultButtonSectionProps) { + return ( +
+ +
+ ); +} diff --git a/src/features/eligibility/ui/school/index.ts b/src/features/eligibility/ui/school/index.ts new file mode 100644 index 0000000..6e8ea7b --- /dev/null +++ b/src/features/eligibility/ui/school/index.ts @@ -0,0 +1,6 @@ +export { SchoolSearchHeader } from "./SchoolSearchHeader"; +export { SchoolSearchFormSection } from "./SchoolSearchFormSection"; +export { SchoolSearchResultButtonSection } from "./SchoolSearchResultButtonSection"; +export { SchoolResultHeader } from "./SchoolResultHeader"; +export { SchoolResultContentSection } from "./SchoolResultContentSection"; +export { SchoolResultTryAgainSection } from "./SchoolResultTryAgainSection"; diff --git a/src/shared/ui/searchBarLabel/searchBarLabel.tsx b/src/shared/ui/searchBarLabel/searchBarLabel.tsx index 5868ed4..e312c92 100644 --- a/src/shared/ui/searchBarLabel/searchBarLabel.tsx +++ b/src/shared/ui/searchBarLabel/searchBarLabel.tsx @@ -45,7 +45,14 @@ export const SearchBarLabel = ({
- +
) : ( @@ -56,7 +63,14 @@ export const SearchBarLabel = ({
- +
)} From 8cbd982efb5aeebde27121c0e694ce89d7b94de8 Mon Sep 17 00:00:00 2001 From: JW-Ahn0 Date: Tue, 24 Feb 2026 20:29:42 +0900 Subject: [PATCH 2/5] =?UTF-8?q?fix:=20/eligibility=20=EC=99=B8=20=EB=8B=A4?= =?UTF-8?q?=EB=A5=B8=20=EC=A3=BC=EC=86=8C=EC=97=90=EC=84=9C=20=EB=AA=A8?= =?UTF-8?q?=EB=8B=AC=20=EB=9C=A8=EB=8A=94=20=ED=98=84=EC=83=81=20fix?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/eligibility/page.tsx | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/app/eligibility/page.tsx b/app/eligibility/page.tsx index b6a01dc..8124988 100644 --- a/app/eligibility/page.tsx +++ b/app/eligibility/page.tsx @@ -1,7 +1,7 @@ "use client"; import { Suspense, useEffect, useState } from "react"; -import { useRouter } from "next/navigation"; +import { usePathname, useRouter, useSearchParams } from "next/navigation"; import { EligibilitySection } from "@/src/widgets/eligibilitySection"; import { useEligibilityStore } from "@/src/features/eligibility/model/eligibilityStore"; import { useDiagnosisResultStore } from "@/src/features/eligibility/model/diagnosisResultStore"; @@ -12,6 +12,8 @@ import { Spinner } from "@/src/shared/ui/spinner/default"; export default function EligibilityPage() { const router = useRouter(); + const pathname = usePathname(); + const searchParams = useSearchParams(); const reset = useEligibilityStore(state => state.reset); const setDiagnosisResult = useDiagnosisResultStore(s => s.setResult); const [isModalOpen, setIsModalOpen] = useState(false); @@ -33,7 +35,12 @@ export default function EligibilityPage() { }, { incomeLevel: data.myIncomeLevel } ); - setIsModalOpen(true); + // 주소가 정확히 /eligibility 일 때만 모달 표시 (쿼리 있으면 표시 안 함) + const isEligibilityOnly = + pathname === "/eligibility" && searchParams.toString() === ""; + if (isEligibilityOnly) { + setIsModalOpen(true); + } } else { reset(); } @@ -46,7 +53,7 @@ export default function EligibilityPage() { return () => { mounted = false; }; - }, [reset, setDiagnosisResult]); + }, [pathname, reset, searchParams, setDiagnosisResult]); const handleModalButtonClick = (index: number) => { setIsModalOpen(false); From 1ea3978b1002118eb2aea383659c7e7713de95aa Mon Sep 17 00:00:00 2001 From: JW-Ahn0 Date: Tue, 24 Feb 2026 20:30:13 +0900 Subject: [PATCH 3/5] =?UTF-8?q?feat:=20=ED=95=99=EA=B5=90=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=EC=9A=A9=20help=20Button=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../model/eligibilityDecisionTree.ts | 3 ++- .../ui/common/eligibilityComponentRenderer.tsx | 18 ++++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/src/features/eligibility/model/eligibilityDecisionTree.ts b/src/features/eligibility/model/eligibilityDecisionTree.ts index 51d665b..3094c55 100644 --- a/src/features/eligibility/model/eligibilityDecisionTree.ts +++ b/src/features/eligibility/model/eligibilityDecisionTree.ts @@ -15,6 +15,7 @@ export type ComponentType = | "priceInput" | "numberInputList" | "infoButton" + | "helpButton" | "datePicker" | "checkbox"; @@ -730,7 +731,7 @@ export const eligibilityDecisionTree: StepConfig[] = [ ], }, { - type: "infoButton", + type: "helpButton", props: { title: "우리 학교가 공공 임대주택에서 인정되는 학교 기준에 해당되는지 궁금하다면?", description: diff --git a/src/features/eligibility/ui/common/eligibilityComponentRenderer.tsx b/src/features/eligibility/ui/common/eligibilityComponentRenderer.tsx index 88bee2a..9b89d63 100644 --- a/src/features/eligibility/ui/common/eligibilityComponentRenderer.tsx +++ b/src/features/eligibility/ui/common/eligibilityComponentRenderer.tsx @@ -10,6 +10,7 @@ import { EligibilitySelect } from "./eligibilitySelect"; import { EligibilityPriceInput } from "./eligibilityPriceInput"; import { EligibilityNumberInputList } from "./eligibilityNumberInputList"; import { EligibilityInfoButtonWithSheet } from "./eligibilityInfoButtonWithSheet"; +import { EligibilityHelpButton } from "./eligibilityHelpButton"; import { DatePicker } from "@/src/shared/ui/datePicker/datePicker"; import { Checkbox } from "@/src/shared/lib/headlessUi/checkBox/checkbox"; import { motion, AnimatePresence } from "framer-motion"; @@ -321,6 +322,23 @@ export const EligibilityComponentRenderer = ({ config }: EligibilityComponentRen ); } + case "helpButton": { + let onClick = config.props.onClick; + if (config.props.action === "home") { + onClick = () => router.push("/home"); + } else if (config.props.action === "back") { + onClick = () => router.back(); + } else if (config.props.action === "path") { + onClick = () => router.push(config.props.path); + } + return ( + + ); + } case "datePicker": { const value = config.storeKey ? getStoreValue(config.storeKey) : undefined; const setter = config.storeKey ? getStoreSetter(config.storeKey) : undefined; From 3470798b1d357c3d5aba83b89b6985f8551289af Mon Sep 17 00:00:00 2001 From: JW-Ahn0 Date: Tue, 24 Feb 2026 20:37:52 +0900 Subject: [PATCH 4/5] =?UTF-8?q?style:=20helpButton=20padding=20=EC=A1=B0?= =?UTF-8?q?=EC=A0=88(py-5=20->=20pb-5)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../eligibility/ui/steps/eligibilityStepRenderer.tsx | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/features/eligibility/ui/steps/eligibilityStepRenderer.tsx b/src/features/eligibility/ui/steps/eligibilityStepRenderer.tsx index c227897..b040ce3 100644 --- a/src/features/eligibility/ui/steps/eligibilityStepRenderer.tsx +++ b/src/features/eligibility/ui/steps/eligibilityStepRenderer.tsx @@ -39,7 +39,15 @@ export const EligibilityStepRenderer = ({ stepId, className }: EligibilityStepRe )} {/* 컴포넌트 렌더링 */} -
+
From a40b3bbd92bbb48fd737559e41fc24e6eeddf0c6 Mon Sep 17 00:00:00 2001 From: chan Date: Tue, 24 Feb 2026 21:41:18 +0900 Subject: [PATCH 5/5] =?UTF-8?q?chore:=20npm=20=EC=9D=B8=EC=A6=9D=20secret?= =?UTF-8?q?=20=EC=A0=84=EB=8B=AC=20=EB=B0=A9=EC=8B=9D=20=EB=B6=88=EC=9D=BC?= =?UTF-8?q?=EC=B9=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci.yml | 2 +- Docker-pinhouse-file | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 69dd578..f21b86a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -35,6 +35,6 @@ jobs: NEXT_PUBLIC_API_URL=${{ secrets.NEXT_PUBLIC_API_URL }} NEXT_PUBLIC_OAUTH2=${{ secrets.NEXT_PUBLIC_OAUTH2 }} secrets: | - npmrc=${{ secrets.NPM_TOKEN }} + NPM_TOKEN=${{ secrets.NPM_TOKEN }} cache-from: type=gha cache-to: type=gha,mode=max diff --git a/Docker-pinhouse-file b/Docker-pinhouse-file index 14d209f..09e6dbd 100644 --- a/Docker-pinhouse-file +++ b/Docker-pinhouse-file @@ -10,8 +10,9 @@ WORKDIR /pinhouse-fe # 2️⃣ Dependencies # ========================= FROM base AS deps -COPY package.json package-lock.json ./ -RUN --mount=type=secret,id=npmrc,target=/root/.npmrc npm ci +COPY package.json package-lock.json .npmrc ./ +RUN --mount=type=secret,id=NPM_TOKEN \ + NPM_TOKEN="$(cat /run/secrets/NPM_TOKEN)" npm ci # ========================= # 3️⃣ Build