Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 11 additions & 4 deletions app/eligibility/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { EligibilitySection } from "@/src/widgets/eligibilitySection";
import { useEligibilityStore } from "@/src/features/eligibility/model/eligibilityStore";
import { useDiagnosisResultStore } from "@/src/features/eligibility/model/diagnosisResultStore";
import { getDiagnosisLatest } from "@/src/features/eligibility/api/diagnosisApi";
import type { DiagnosisResultData } from "@/src/features/eligibility/api/diagnosisTypes";
import type { DiagnosisLatestData } from "@/src/features/eligibility/api/diagnosisTypes";
import { Modal } from "@/src/shared/ui/modal/default/modal";
import { Spinner } from "@/src/shared/ui/spinner/default";

Expand All @@ -21,11 +21,18 @@ export default function EligibilityPage() {

const checkLatest = async () => {
try {
const response = await getDiagnosisLatest<DiagnosisResultData>();
const response = await getDiagnosisLatest<DiagnosisLatestData>();
if (!mounted) return;
const data = response?.data;
const data = response?.data as DiagnosisLatestData | undefined;
if (data != null && typeof data === "object" && "eligible" in data) {
setDiagnosisResult(data as DiagnosisResultData);
setDiagnosisResult(
{
eligible: data.eligible,
decisionMessage: data.diagnosisResult,
recommended: data.recommended,
},
{ incomeLevel: data.myIncomeLevel }
);
setIsModalOpen(true);
} else {
reset();
Expand Down
18 changes: 16 additions & 2 deletions src/assets/images/eligibility/resultBannerImg.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,20 @@
const ResultBannerImg = () => {
import { SVGProps } from "react";

interface ResultBannerImgProps extends SVGProps<SVGSVGElement> {
width?: number | string;
height?: number | string;
}

const ResultBannerImg = ({ width = 96, height = 96, ...props }: ResultBannerImgProps) => {
return (
<svg width="96" height="96" viewBox="0 0 96 96" fill="none" xmlns="http://www.w3.org/2000/svg">
<svg
width={width}
height={height}
viewBox="0 0 96 96"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path
d="M48.8273 75.8059L52.6109 62.0161H42.4414L46.2251 75.8059C46.5892 77.1313 48.4622 77.1313 48.8264 75.8059H48.8273Z"
fill="url(#paint0_linear_10635_91474)"
Expand Down
6 changes: 3 additions & 3 deletions src/features/eligibility/api/diagnosisApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@ import {
DIAGNOSIS_LATEST_ENDPOINT,
} from "@/src/shared/api/endpoints";
import type { IResponse } from "@/src/shared/types/response";
import type { DiagnosisResultData } from "./diagnosisTypes";
import type { DiagnosisLatestData, DiagnosisResultData } from "./diagnosisTypes";
import type { DiagnosisPostRequest } from "./diagnosisTypes";

const v2Options = { baseURL: API_BASE_URL_V2 };

/** GET /v2/diagnosis/latest - 청약 진단 최신 결과 조회 */
export function getDiagnosisLatest<T = DiagnosisResultData>() {
/** GET /v2/diagnosis/latest - 청약 진단 최신 결과 조회 (POST 응답과 구조 상이) */
export function getDiagnosisLatest<T = DiagnosisLatestData>() {
return http.get<IResponse<T>>(DIAGNOSIS_LATEST_ENDPOINT, undefined, v2Options);
}

Expand Down
17 changes: 16 additions & 1 deletion src/features/eligibility/api/diagnosisTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ export interface DiagnosisPostRequest {
hasSpecialCategory: DiagnosisSpecialCategory[];
}

/** POST /v2/diagnosis 응답 data (공통 제외). GET /diagnosis/latest 응답에 소득분위·지원대상이 포함 */
/** POST /v2/diagnosis 응답 data (공통 제외) */
export interface DiagnosisResultData {
eligible: boolean;
decisionMessage: string;
Expand All @@ -119,3 +119,18 @@ export interface DiagnosisResultData {
/** 나의 지원 가능 대상 */
targetGroups?: string[];
}

/** GET /v2/diagnosis/latest 응답 data (POST 응답과 별도 구조) */
export interface DiagnosisLatestData {
age: number;
availableRentalTypes: string[];
availableSupplyTypes: string[];
diagnosedAt: string;
diagnosisId: number;
diagnosisResult: string;
eligible: boolean;
gender: string;
myIncomeLevel: string;
nickname: string;
recommended: string[];
}
6 changes: 3 additions & 3 deletions src/features/eligibility/hooks/useDiagnosisLatest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import { useQuery } from "@tanstack/react-query";
import { getDiagnosisLatest } from "../api/diagnosisApi";
import type { DiagnosisResultData } from "../api/diagnosisTypes";
import type { DiagnosisLatestData } from "../api/diagnosisTypes";

const QUERY_KEY = ["diagnosis", "latest"];

Expand All @@ -11,8 +11,8 @@ export function useDiagnosisLatest() {
const query = useQuery({
queryKey: QUERY_KEY,
queryFn: async () => {
const res = await getDiagnosisLatest<DiagnosisResultData>();
return (res?.data ?? null) as DiagnosisResultData | null;
const res = await getDiagnosisLatest<DiagnosisLatestData>();
return (res?.data ?? null) as DiagnosisLatestData | null;
},
retry: false,
});
Expand Down
10 changes: 4 additions & 6 deletions src/features/eligibility/ui/result/diagnosisResultBanner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,24 +35,22 @@ export const DiagnosisResultBanner = ({
return (
<div
className={cn(
"flex gap-4 rounded-lg bg-white px-5 py-4 shadow-result-card",
"shadow-result-card flex gap-4 rounded-lg border border-solid border-greyscale-grey-75 bg-white px-5 py-4",
className
)}
role="banner"
>
<div className="relative flex-shrink-0">
<ResultBannerImg />
<ResultBannerImg width={60} height={60} />
</div>
<div className="flex min-w-0 flex-1 flex-col justify-center gap-1">
<p className="text-sm font-medium leading-[140%] tracking-[-0.02em] text-greyscale-grey-900">
<span className="underline decoration-greyscale-grey-300 underline-offset-2">
{userName}
{userName}님은 <br />
</span>
님은 소득{" "}
<span className="underline decoration-greyscale-grey-300 underline-offset-2">
{bunwi}
소득 {bunwi}
</span>
이고,
</p>
<p className="text-sm font-medium leading-[140%] tracking-[-0.02em] text-greyscale-grey-900">
<span className="underline decoration-greyscale-grey-300 underline-offset-2">
Expand Down
19 changes: 9 additions & 10 deletions src/features/eligibility/ui/result/diagnosisResultItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,33 +26,32 @@ export const DiagnosisResultItem = ({
return (
<article
className={cn(
"flex flex-col gap-3 rounded-lg bg-white px-5 py-4 shadow-result-card",
"shadow-result-card flex flex-col gap-3 rounded-lg border border-solid border-greyscale-grey-75 bg-white",
className
)}
>
<div className="flex flex-col gap-3">
<div className="flex flex-row items-center gap-3 px-3.5 py-3.5">
<span
className={cn(
"w-fit rounded-full px-3 py-1 text-xs font-medium",
tagClass
tagClass,
"flex min-w-[60px] shrink-0 items-center justify-center rounded-lg px-1 py-[3px] text-xs-10 font-semibold tracking-[-0.01em]"
)}
>
{housingType}
</span>
<p className="text-sm leading-[140%] tracking-[-0.02em] text-greyscale-grey-700">
<p className="text-xs-10 font-medium leading-[134%] text-greyscale-grey-800">
{description}
</p>
</div>
<div className="h-px bg-greyscale-grey-100" aria-hidden />
<div className="flex flex-col gap-2">
<span className="text-xs font-medium text-greyscale-grey-500">
<div className="flex flex-row gap-3 rounded-b-lg bg-greyscale-grey-25 px-3.5 py-3.5">
<span className="shrink-0 text-xs-10 font-semibold leading-[140%] tracking-[-0.02em] text-greyscale-grey-600">
신청 가능 유형
</span>
<div className="flex flex-wrap gap-2">
<div className="flex flex-wrap gap-1">
{applicationTypes.map((type, index) => (
<span
key={`${type}-${index}`}
className="rounded-full bg-greyscale-grey-100 px-3 py-1 text-xs font-medium text-greyscale-grey-700"
className="font-regular rounded border border-greyscale-grey-100 bg-white px-1 py-[0.75px] text-xs-10 tracking-[-0.01em] text-greyscale-grey-600"
>
{type}
</span>
Expand Down
10 changes: 3 additions & 7 deletions src/features/eligibility/ui/result/diagnosisResultList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,7 @@ function groupByHousingType(
}));
}

export const DiagnosisResultList = ({
recommended,
className,
}: DiagnosisResultListProps) => {
export const DiagnosisResultList = ({ recommended, className }: DiagnosisResultListProps) => {
const items = useMemo(() => groupByHousingType(recommended), [recommended]);

if (items.length === 0) return null;
Expand All @@ -54,7 +51,7 @@ export const DiagnosisResultList = ({
<section className={className} aria-labelledby="diagnosis-result-list-title">
<h2
id="diagnosis-result-list-title"
className="mb-3 text-lg font-bold leading-[140%] tracking-[-0.02em] text-greyscale-grey-900"
className="mb-3 text-base font-semibold leading-4 tracking-[-0.01em] text-greyscale-grey-900"
>
{SECTION_TITLE}
</h2>
Expand All @@ -64,8 +61,7 @@ export const DiagnosisResultList = ({
<DiagnosisResultItem
housingType={housingType}
description={
HOUSING_TYPE_DESCRIPTIONS[housingType] ??
"해당 유형의 공공임대주택입니다."
HOUSING_TYPE_DESCRIPTIONS[housingType] ?? "해당 유형의 공공임대주택입니다."
}
applicationTypes={applicationTypes}
/>
Expand Down
73 changes: 24 additions & 49 deletions src/features/mypage/ui/pinReportSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,31 +13,28 @@ import {
MYPAGE_PIN_REPORT_TARGET_LABEL,
MYPAGE_PIN_REPORT_HOUSING_LABEL,
} from "@/src/features/mypage/model/mypageConstants";
import type { DiagnosisResultData } from "@/src/features/eligibility/api/diagnosisTypes";
import { useDiagnosisResultStore } from "@/src/features/eligibility/model/diagnosisResultStore";
import type { DiagnosisLatestData } from "@/src/features/eligibility/api/diagnosisTypes";

interface PinReportSectionProps {
/** 자격진단 최신 결과. 있으면 요약 카드, 없으면 '자격진단 하러가기' 빈 상태 표시 */
diagnosisResult: DiagnosisResultData | null;
/** GET /diagnosis/latest 결과. 있으면 요약 카드, 없으면 '자격진단 하러가기' 빈 상태 표시 */
diagnosisResult: DiagnosisLatestData | null;
onDiagnosisClick?: () => void;
onRediagnosisClick?: () => void;
onViewDetailClick?: () => void;
}

const PIN_REPORT_HEADING_ID = "pin-report-heading";
const TAG_CLASS =
"inline-flex items-center rounded-full bg-primary-blue-100 px-2.5 py-1 text-xs font-medium text-primary-blue-700";
"inline-flex items-center rounded-full bg-primary-blue-25 px-2.5 py-1 text-xs font-medium text-primary-blue-400";

/** recommended 항목에서 housingType만 추출 (예: "통합공공임대 : 청년 특별공급" → "통합공공임대") */
function getUniqueHousingTypes(recommended: string[]): string[] {
const set = new Set<string>();
const sep = " : ";
for (const raw of recommended) {
const idx = raw.indexOf(sep);
const housingType = (idx === -1 ? raw : raw.slice(0, idx)).trim();
if (housingType) set.add(housingType);
/** 나의 지원 가능 대상: gender + availableSupplyTypes */
function getTargetGroupLabels(data: DiagnosisLatestData): string[] {
const list: string[] = [];
if (data.gender) list.push(data.gender);
if (Array.isArray(data.availableSupplyTypes)) {
list.push(...data.availableSupplyTypes);
}
return Array.from(set);
return list;
}

export const PinReportSection = ({
Expand All @@ -46,19 +43,16 @@ export const PinReportSection = ({
onRediagnosisClick,
onViewDetailClick,
}: PinReportSectionProps) => {
const storeIncomeLevel = useDiagnosisResultStore(s => s.incomeLevel);

const hasResult = diagnosisResult != null && Array.isArray(diagnosisResult.recommended);
const incomeLevel =
diagnosisResult?.incomeLevel ?? storeIncomeLevel ?? null;
const targetGroups = diagnosisResult?.targetGroups ?? [];
const housingTypes = useMemo(
() =>
hasResult && diagnosisResult?.recommended?.length
? getUniqueHousingTypes(diagnosisResult.recommended)
: [],
[hasResult, diagnosisResult?.recommended]
const hasResult =
diagnosisResult != null &&
Array.isArray(diagnosisResult.recommended) &&
diagnosisResult.recommended.length > 0;
const incomeLevel = diagnosisResult?.myIncomeLevel ?? null;
const targetGroups = useMemo(
() => (diagnosisResult ? getTargetGroupLabels(diagnosisResult) : []),
[diagnosisResult]
);
const housingTypes = diagnosisResult?.availableRentalTypes ?? [];

if (hasResult) {
return (
Expand Down Expand Up @@ -86,31 +80,15 @@ export const PinReportSection = ({
<div className="flex flex-col gap-4 px-4 py-5">
{incomeLevel != null && incomeLevel !== "" && (
<div>
<h3 className="mb-2 text-sm font-bold leading-[140%] tracking-[-0.02em] text-greyscale-grey-900">
<h3 className="mb-2 text-xs-12 font-medium leading-[140%] tracking-[-0.02em] text-greyscale-grey-700">
{MYPAGE_PIN_REPORT_INCOME_LABEL}
</h3>
<span className={TAG_CLASS}>{incomeLevel}</span>
</div>
)}

{targetGroups.length > 0 && (
<div>
<h3 className="mb-2 text-sm font-bold leading-[140%] tracking-[-0.02em] text-greyscale-grey-900">
{MYPAGE_PIN_REPORT_TARGET_LABEL}
</h3>
<div className="flex flex-wrap gap-2">
{targetGroups.map(label => (
<span key={label} className={TAG_CLASS}>
{label}
</span>
))}
</div>
</div>
)}

{housingTypes.length > 0 && (
<div>
<h3 className="mb-2 text-sm font-bold leading-[140%] tracking-[-0.02em] text-greyscale-grey-900">
<h3 className="mb-2 text-xs-12 font-medium leading-[140%] tracking-[-0.02em] text-greyscale-grey-700">
{MYPAGE_PIN_REPORT_HOUSING_LABEL}
</h3>
<div className="flex flex-wrap gap-2">
Expand All @@ -128,7 +106,7 @@ export const PinReportSection = ({
<button
type="button"
onClick={onViewDetailClick}
className="flex items-center gap-1 text-sm font-bold text-primary-blue-600 hover:underline"
className="text-primary-blue-600 flex items-center gap-1 text-sm font-bold hover:underline"
>
{MYPAGE_PIN_REPORT_VIEW_DETAIL}
<ChevronRight className="h-4 w-4" aria-hidden />
Expand All @@ -141,10 +119,7 @@ export const PinReportSection = ({
}

return (
<section
aria-labelledby={PIN_REPORT_HEADING_ID}
className="flex flex-col rounded-lg bg-white"
>
<section aria-labelledby={PIN_REPORT_HEADING_ID} className="flex flex-col rounded-lg bg-white">
<div className="px-4 py-4">
<h2
id={PIN_REPORT_HEADING_ID}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ export const EligibilityFinalResultSection = () => {
onRightClick={() => router.push("/home")}
/>
</header>
<div className="flex flex-1 flex-col gap-4 bg-greyscale-grey-25 px-5 pb-5">
<div className="flex flex-1 flex-col gap-4 px-5 pb-5">
<DiagnosisResultBanner
userName={userName ?? "회원"}
incomeLevel={incomeLevel}
Expand Down
11 changes: 8 additions & 3 deletions src/widgets/mypageSection/ui/MypageSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,14 @@ export const MypageSection = () => {

useEffect(() => {
if (diagnosisLatest) {
setDiagnosisResult(diagnosisLatest, {
incomeLevel: diagnosisLatest.incomeLevel,
});
setDiagnosisResult(
{
eligible: diagnosisLatest.eligible,
decisionMessage: diagnosisLatest.diagnosisResult,
recommended: diagnosisLatest.recommended,
},
{ incomeLevel: diagnosisLatest.myIncomeLevel }
);
}
}, [diagnosisLatest, setDiagnosisResult]);

Expand Down