Skip to content

Commit f4d359d

Browse files
authored
Merge branch 'dev' into design/cardList-yeji
2 parents 716de08 + 3ee9862 commit f4d359d

26 files changed

+1021
-44
lines changed

src/app/components/button/dropdown/FilterDropdown.tsx

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,9 @@ const FilterDropdown = ({ options, className = "" }: FilterDropdownProps) => {
3030
className={cn(
3131
"flex w-full items-center justify-between rounded-md border p-2 font-medium shadow-sm",
3232
"text-gray-700 hover:bg-primary-orange-50",
33-
selectedLabel === options[0] ? "border-gray-50 bg-white" : "border-primary-orange-300 bg-primary-orange-50"
33+
selectedLabel === options[0]
34+
? "border border-gray-100 bg-white"
35+
: "border-primary-orange-300 bg-primary-orange-50"
3436
)}
3537
onClick={toggleDropdown}
3638
>
@@ -47,7 +49,14 @@ const FilterDropdown = ({ options, className = "" }: FilterDropdownProps) => {
4749
</button>
4850
</div>
4951

50-
{isOpen && <DropdownList list={options} onSelect={handleSelect} wrapperStyle="h-full" />}
52+
{isOpen && (
53+
<DropdownList
54+
list={options}
55+
onSelect={handleSelect}
56+
wrapperStyle="h-full w-[80px] md:w-[126px]"
57+
itemStyle="md:text-lg text-xs "
58+
/>
59+
)}
5160
</div>
5261
);
5362
};

src/app/components/button/dropdown/dropdownComponent/DropdownList.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,9 @@ const DropdownList = ({
3838
role="menu"
3939
aria-orientation="vertical"
4040
aria-labelledby="options-menu"
41-
className="mt-[6px] rounded border border-gray-100 bg-gray-50 pr-[2px] pt-1"
41+
className="absolute mt-[6px] rounded border border-gray-100 bg-gray-50 pr-[2px] pt-1"
4242
>
43-
<ul className={`flex w-full flex-col overflow-hidden lg:w-[126px] ${wrapperStyle} scrollbar-custom`}>
43+
<ul className={`flex flex-col overflow-hidden ${wrapperStyle} scrollbar-custom`}>
4444
{list.map((item) => (
4545
<DropdownItem key={item} item={item} onSelect={onSelect} itemStyle={itemStyle} />
4646
))}
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
import { getRecruitmentStatus } from "@/utils/recruitDateFormatter";
2+
import { formatRecruitDate } from "@/utils/workDayFormatter";
3+
import Chip from "@/app/components/chip/Chip";
4+
import Image from "next/image";
5+
import { applicationStatus, ApplicationStatus } from "@/types/application";
6+
import axios from "axios";
7+
import toast from "react-hot-toast";
8+
9+
interface Owner {
10+
imageUrl: string;
11+
storeName: string;
12+
id: number;
13+
}
14+
15+
interface Form {
16+
owner: Owner;
17+
recruitmentEndDate: string;
18+
recruitmentStartDate: string;
19+
description: string;
20+
title: string;
21+
id: number;
22+
}
23+
24+
interface ApplicationListItemProps {
25+
updatedAt: Date;
26+
createdAt: Date;
27+
status: ApplicationStatus;
28+
resumeName: string;
29+
resumeId: number;
30+
form: Form;
31+
id: number;
32+
}
33+
34+
const getStatusVariant = (status: ApplicationStatus) => {
35+
switch (status) {
36+
case applicationStatus.HIRED:
37+
return "positive";
38+
case applicationStatus.REJECTED:
39+
return "negative";
40+
default:
41+
return "positive";
42+
}
43+
};
44+
45+
const getStatusLabel = (status: ApplicationStatus) => {
46+
switch (status) {
47+
case applicationStatus.HIRED:
48+
return "채용 완료";
49+
case applicationStatus.REJECTED:
50+
return "거절";
51+
case applicationStatus.INTERVIEW_PENDING:
52+
return "면접 대기";
53+
case applicationStatus.INTERVIEW_COMPLETED:
54+
return "면접 완료";
55+
default:
56+
return status;
57+
}
58+
};
59+
60+
const MyApplicationListItem = ({ createdAt, status, resumeId, resumeName, form }: ApplicationListItemProps) => {
61+
const recruitmentStatus = getRecruitmentStatus(new Date(form.recruitmentEndDate));
62+
63+
const handleResumeDownload = async () => {
64+
try {
65+
const response = await axios.get(`/api/resumes/${resumeId}`, {
66+
responseType: "blob",
67+
});
68+
69+
const blob = new Blob([response.data], { type: "application/pdf" });
70+
const url = window.URL.createObjectURL(blob);
71+
72+
const link = document.createElement("a");
73+
link.href = url;
74+
link.download = resumeName || `이력서_${resumeId}.pdf`;
75+
document.body.appendChild(link);
76+
link.click();
77+
78+
window.URL.revokeObjectURL(url);
79+
document.body.removeChild(link);
80+
81+
toast.success("이력서가 다운로드되었습니다.");
82+
} catch (error) {
83+
console.error("Resume download error:", error);
84+
toast.error("이력서 다운로드에 실패했습니다.");
85+
}
86+
};
87+
88+
return (
89+
<div className="relative h-auto w-full overflow-hidden rounded-xl border border-gray-200 bg-white p-6 shadow-sm transition-transform duration-300 hover:scale-[1.02] sm:h-[219px] sm:w-[375px] md:h-[328px] md:w-[477px]">
90+
<div className="flex h-full flex-col">
91+
{/* 상단 영역: 지원일시와 이력서 링크 */}
92+
<div className="flex items-center justify-between">
93+
<div className="flex items-center gap-2 text-sm text-gray-500 md:text-base">
94+
<span>지원일시</span>
95+
<span>|</span>
96+
<span>{formatRecruitDate(createdAt, true)}</span>
97+
</div>
98+
<button
99+
onClick={handleResumeDownload}
100+
className="text-sm font-medium text-gray-500 underline decoration-gray-600/50 decoration-1 underline-offset-4 hover:cursor-pointer hover:text-gray-600 hover:decoration-gray-600 md:text-base"
101+
>
102+
이력서 보기
103+
</button>
104+
</div>
105+
106+
{/* 중앙 컨텐츠 영역 */}
107+
<div className="flex-1 space-y-3 py-4">
108+
{/* 가게 정보 영역 */}
109+
<div className="flex items-center gap-2">
110+
<div className="relative h-10 w-10 overflow-hidden rounded-full md:h-14 md:w-14">
111+
<Image src={form.owner.imageUrl} alt={form.owner.storeName} fill className="object-cover" />
112+
</div>
113+
<span className="text-base font-medium text-gray-900 md:text-lg">{form.owner.storeName}</span>
114+
</div>
115+
116+
{/* 제목 */}
117+
<div className="text-lg font-bold text-gray-900 md:text-xl">{form.title}</div>
118+
119+
{/* 설명 */}
120+
<p className="line-clamp-2 text-sm text-gray-600 md:line-clamp-2 md:text-base">{form.description}</p>
121+
</div>
122+
123+
{/* 하단 상태 칩 영역 */}
124+
<div className="flex gap-2">
125+
<div className="rounded-[4px] border border-primary-orange-300 md:text-base">
126+
<Chip label={getStatusLabel(status)} variant={getStatusVariant(status)} />
127+
</div>
128+
<div className="rounded-[4px] border border-primary-orange-300 md:text-base">
129+
<Chip label={recruitmentStatus} variant={recruitmentStatus === "모집 중" ? "positive" : "negative"} />
130+
</div>
131+
</div>
132+
</div>
133+
</div>
134+
);
135+
};
136+
137+
export default MyApplicationListItem;
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import { formatRecruitDate, getWorkDaysDisplay } from "@/utils/workDayFormatter";
2+
import RecruitConditionItem from "./RecruitConditionItem";
3+
4+
interface RecruitConditionProps {
5+
hourlyWage: number;
6+
recruitmentStartDate: Date;
7+
recruitmentEndDate: Date;
8+
isNegotiableWorkDays: boolean;
9+
workDays?: string[];
10+
workStartTime: string;
11+
workEndTime: string;
12+
}
13+
14+
export const RecruitCondition = ({
15+
hourlyWage,
16+
recruitmentStartDate,
17+
recruitmentEndDate,
18+
isNegotiableWorkDays,
19+
workDays = [],
20+
workStartTime,
21+
workEndTime,
22+
}: RecruitConditionProps) => {
23+
const periodValue = (
24+
<>
25+
<span className="whitespace-normal md:hidden">
26+
{formatRecruitDate(recruitmentStartDate)}~{formatRecruitDate(recruitmentEndDate)}
27+
</span>
28+
<span className="hidden whitespace-normal md:inline">
29+
{formatRecruitDate(recruitmentStartDate, true)}~
30+
<br />
31+
{formatRecruitDate(recruitmentEndDate, true)}
32+
</span>
33+
</>
34+
);
35+
36+
const conditions = [
37+
{
38+
icon: {
39+
sm: "/icons/coin/coin-sm.svg",
40+
md: "/icons/coin/coin-md.svg",
41+
},
42+
label: "시급",
43+
value: `${hourlyWage.toLocaleString()}원`,
44+
},
45+
{
46+
icon: {
47+
sm: "/icons/calendar/calendar-clock-sm.svg",
48+
md: "/icons/calendar/calendar-clock-md.svg",
49+
},
50+
label: "기간",
51+
value: periodValue,
52+
},
53+
{
54+
icon: {
55+
sm: "/icons/calendar/calendar-sm.svg",
56+
md: "/icons/calendar/calendar-md.svg",
57+
},
58+
label: "요일",
59+
value: getWorkDaysDisplay(isNegotiableWorkDays, workDays),
60+
},
61+
{
62+
icon: {
63+
sm: "/icons/clock/clock-sm.svg",
64+
md: "/icons/clock/clock-md.svg",
65+
},
66+
label: "시간",
67+
value: `${workStartTime}~${workEndTime}`,
68+
},
69+
];
70+
71+
return (
72+
<div className="h-auto w-full sm:h-[156px] sm:w-[327px] sm:p-3 md:h-[336px] md:w-[640px]">
73+
<div className="grid h-full grid-cols-2 gap-2 sm:gap-3">
74+
{conditions.map((condition, index) => (
75+
<RecruitConditionItem key={index} icon={condition.icon} label={condition.label} value={condition.value} />
76+
))}
77+
</div>
78+
</div>
79+
);
80+
};
81+
82+
export default RecruitCondition;
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import Image from "next/image";
2+
3+
interface RecruitConditionItemProps {
4+
icon: {
5+
sm: string;
6+
md: string;
7+
};
8+
label: string;
9+
value: string | React.ReactNode;
10+
}
11+
12+
const RecruitConditionItem = ({ icon, label, value }: RecruitConditionItemProps) => {
13+
return (
14+
<div className="flex items-center gap-2 overflow-hidden rounded-lg border border-gray-200 p-1 sm:p-3 md:gap-6 md:p-6">
15+
<div className="flex h-6 w-6 items-center justify-center rounded-full bg-gray-100 bg-opacity-30 sm:h-9 sm:w-9 md:h-16 md:w-16">
16+
<div className="block md:hidden">
17+
<Image src={icon.sm} alt={label} width={24} height={24} />
18+
</div>
19+
<div className="hidden md:block">
20+
<Image src={icon.md} alt={label} width={36} height={36} />
21+
</div>
22+
</div>
23+
<div className="flex-1">
24+
<div className="text-[10px] font-medium text-gray-600 sm:text-xs md:text-xl">{label}</div>
25+
<div className="text-xs font-semibold text-primary-orange-300 sm:text-sm md:text-2xl">{value}</div>
26+
</div>
27+
</div>
28+
);
29+
};
30+
31+
export default RecruitConditionItem;

0 commit comments

Comments
 (0)