Skip to content

Commit 4d445a0

Browse files
authored
Merge pull request #112 from FE9-2/feat/albaList
fix: 이미지 URL 및 날짜 검증 로직 추가
2 parents 348eb60 + c268a53 commit 4d445a0

File tree

9 files changed

+92
-48
lines changed

9 files changed

+92
-48
lines changed

src/app/(pages)/albaList/components/SearchSection.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ export default function SearchSection() {
2525
return (
2626
<form onSubmit={handleSubmit} className="w-full">
2727
<div className="mx-auto flex items-center justify-between gap-4">
28-
<div className="w-[270px] md:w-[500px] lg:w-[700px] xl:w-[900px] 2xl:w-[1100px]">
28+
<div className="w-[270px] md:w-[500px] lg:w-[700px] xl:w-[900px]">
2929
<SearchInput
3030
value={keyword}
3131
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setKeyword(e.target.value)}

src/app/(pages)/albaList/layout.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import React, { Suspense } from "react";
22

33
export default function AlbaListLayout({ children }: { children: React.ReactNode }) {
44
return (
5-
<div className="mx-auto max-w-screen-2xl px-4 py-8">
5+
<div className="mx-auto max-w-screen-xl px-4 py-8">
66
<Suspense
77
fallback={
88
<div className="flex h-[calc(100vh-200px)] items-center justify-center">

src/app/(pages)/albaList/page.tsx

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import SearchSection from "./components/SearchSection";
1212
import { useUser } from "@/hooks/queries/user/me/useUser";
1313
import Link from "next/link";
1414
import { IoAdd } from "react-icons/io5";
15+
import { userRoles } from "@/constants/userRoles";
1516

1617
const FORMS_PER_PAGE = 10;
1718

@@ -20,7 +21,7 @@ export default function AlbaList() {
2021
const pathname = usePathname();
2122
const searchParams = useSearchParams();
2223
const { user } = useUser();
23-
const isOwner = user?.role === "owner";
24+
const isOwner = user?.role === userRoles.OWNER;
2425

2526
// URL 쿼리 파라미터에서 필터 상태와 키워드 가져오기
2627
const isRecruiting = searchParams.get("isRecruiting");
@@ -124,10 +125,10 @@ export default function AlbaList() {
124125
</div>
125126

126127
{/* 메인 콘텐츠 영역 */}
127-
<div className="w-full pt-[224px]">
128+
<div className="w-full pt-[132px]">
128129
{/* 폼 만들기 버튼 - 고정 위치 */}
129130
{isOwner && (
130-
<div className="fixed bottom-[28%] right-8 z-[9999] translate-y-1/2 md:right-12 lg:right-16 xl:right-20">
131+
<div className="fixed bottom-[50%] right-4 z-[9999] translate-y-1/2">
131132
<Link
132133
href="/addForm"
133134
className="flex items-center gap-2 rounded-lg bg-[#FFB800] px-4 py-3 text-base font-semibold text-white shadow-lg transition-all hover:bg-[#FFA800] md:px-6 md:text-lg"
@@ -143,8 +144,8 @@ export default function AlbaList() {
143144
<p className="text-grayscale-500">등록된 알바 공고가 없습니다.</p>
144145
</div>
145146
) : (
146-
<div className="mx-auto mt-4 w-full max-w-screen-2xl px-4 md:px-6 lg:px-8">
147-
<div className="flex flex-wrap items-center justify-center gap-6">
147+
<div className="mx-auto mt-4 w-full max-w-screen-xl px-3">
148+
<div className="flex flex-wrap justify-start gap-6">
148149
{data?.pages.map((page) => (
149150
<React.Fragment key={page.nextCursor}>
150151
{page.data.map((form) => (

src/app/components/card/cardList/AlbaListItem.tsx

Lines changed: 34 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import useModalStore from "@/store/modalStore";
99
import Indicator from "../../pagination/Indicator";
1010
import { FormListType } from "@/types/response/form";
1111
import { useFormScrap } from "@/hooks/queries/form/useFormScap";
12+
import { MdOutlineImage } from "react-icons/md";
13+
import { S3_URL } from "@/constants/config";
1214

1315
/**
1416
* 알바폼 리스트 아이템 컴포넌트
@@ -31,6 +33,7 @@ const AlbaListItem = ({
3133
const [showDropdown, setShowDropdown] = useState(false); // 드롭다운 메뉴 표시 상태
3234
const [currentImageIndex, setCurrentImageIndex] = useState(0); // 현재 이미지 인덱스
3335
const dropdownRef = useRef<HTMLDivElement>(null); // 드롭다운 메뉴 참조
36+
const [imageError, setImageError] = useState(false);
3437

3538
// 모집 상태 및 D-day 계산
3639
const recruitmentStatus = getRecruitmentStatus(recruitmentEndDate);
@@ -81,22 +84,37 @@ const AlbaListItem = ({
8184
});
8285
};
8386

87+
// S3 URL 체크 함수
88+
const isValidS3Url = (url: string) => {
89+
return url.startsWith(S3_URL);
90+
};
91+
8492
return (
85-
<div className="relative h-[360px] w-[327px] overflow-hidden rounded-xl border border-grayscale-200 bg-white shadow-md transition-transform duration-300 hover:scale-[1.02] lg:h-[536px] lg:w-[477px]">
93+
<div className="relative h-auto w-[327px] overflow-hidden rounded-xl border border-grayscale-200 bg-white shadow-md transition-transform duration-300 hover:scale-[1.02] lg:w-[372px]">
8694
{/* 이미지 슬라이더 영역 */}
87-
<div className="relative h-[200px] overflow-hidden rounded-t-xl lg:h-[310px]">
88-
{/* 현재 이미지 */}
89-
{imageUrls[currentImageIndex] && (
90-
<Image
91-
src={imageUrls[currentImageIndex]}
92-
alt={`Recruit Image ${currentImageIndex + 1}`}
93-
fill
94-
className="object-cover transition-opacity duration-300"
95-
/>
95+
<div className="relative h-[200px] overflow-hidden rounded-t-xl lg:h-[240px]">
96+
{imageUrls[currentImageIndex] && !imageError ? (
97+
isValidS3Url(imageUrls[currentImageIndex]) ? (
98+
<Image
99+
src={imageUrls[currentImageIndex]}
100+
alt={`Recruit Image ${currentImageIndex + 1}`}
101+
fill
102+
className="object-cover transition-opacity duration-300"
103+
onError={() => setImageError(true)}
104+
/>
105+
) : (
106+
<div className="flex h-full w-full items-center justify-center bg-grayscale-100">
107+
<MdOutlineImage className="size-20 text-grayscale-400" />
108+
</div>
109+
)
110+
) : (
111+
<div className="flex h-full w-full items-center justify-center bg-grayscale-100">
112+
<MdOutlineImage className="size-20 text-grayscale-400" />
113+
</div>
96114
)}
97115

98-
{/* 이미지 인디케이터 */}
99-
{imageUrls.length > 1 && (
116+
{/* 이미지 인디케이터 - 유효한 이미지가 2개 이상이고 에러가 없을 때만 표시 */}
117+
{imageUrls.filter((url) => isValidS3Url(url)).length > 1 && !imageError && (
100118
<div className="absolute bottom-4 left-1/2 -translate-x-1/2">
101119
<Indicator
102120
imageCount={imageUrls.length}
@@ -108,7 +126,7 @@ const AlbaListItem = ({
108126
</div>
109127

110128
{/* 콘텐츠 영역 */}
111-
<div className="relative flex h-[140px] flex-col justify-between p-4 lg:h-[226px] lg:p-6">
129+
<div className="relative flex h-[140px] flex-col justify-between p-2 lg:h-[160px]">
112130
{/* 상단 영역 */}
113131
<div className="flex flex-col gap-4">
114132
{/* 상태 표시 영역 (공개여부, 모집상태, 날짜) */}
@@ -117,7 +135,7 @@ const AlbaListItem = ({
117135
<div className="flex items-center justify-between">
118136
<Chip label={isPublic ? "공개" : "비공개"} variant={isPublic ? "positive" : "negative"} />
119137
<Chip label={recruitmentStatus} variant={recruitmentStatus === "모집 중" ? "positive" : "negative"} />
120-
<span className="text-xs font-medium text-grayscale-500 md:inline">
138+
<span className="text-xs font-medium tracking-tighter text-grayscale-500 md:inline lg:text-sm">
121139
{formatRecruitDate(recruitmentStartDate, true)} ~ {formatRecruitDate(recruitmentEndDate, true)}
122140
</span>
123141
</div>
@@ -152,11 +170,11 @@ const AlbaListItem = ({
152170
</div>
153171

154172
{/* 제목 */}
155-
<div className="text-grayscale-900 truncate text-base font-bold lg:text-lg">{title}</div>
173+
<div className="text-grayscale-900 truncate pl-2 text-base font-bold lg:text-lg">{title}</div>
156174
</div>
157175

158176
{/* 통계 정보 영역 - mt-auto 제거하고 부모 컨테이너에 justify-between 추가 */}
159-
<div className="text-grayscale-700 mt-4 flex h-[50px] items-center justify-between rounded-2xl border border-grayscale-100 p-2 text-sm lg:text-base">
177+
<div className="text-grayscale-700 mt-4 flex h-[50px] items-center justify-between rounded-2xl border border-grayscale-100 text-sm lg:text-base">
160178
<div className="flex flex-1 items-center justify-center">
161179
<span className="font-medium">지원자 {applyCount}</span>
162180
</div>

src/app/components/card/cardList/ScrapListItem.tsx

Lines changed: 36 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import Indicator from "../../pagination/Indicator";
99
import { FormListType } from "@/types/response/form";
1010
import { useFormScrap } from "@/hooks/queries/form/useFormScap";
1111
import { useRouter } from "next/navigation";
12+
import { MdOutlineImage } from "react-icons/md";
13+
import { S3_URL } from "@/constants/config";
1214

1315
/**
1416
* 알바폼 스크랩 리스트 아이템 컴포넌트
@@ -30,6 +32,7 @@ const ScrapListItem = ({
3032
const [showDropdown, setShowDropdown] = useState(false); // 드롭다운 메뉴 표시 상태
3133
const [currentImageIndex, setCurrentImageIndex] = useState(0); // 현재 표시 중인 이미지 인덱스
3234
const dropdownRef = useRef<HTMLDivElement>(null); // 드롭다운 메뉴 참조
35+
const [imageError, setImageError] = useState(false);
3336

3437
// 모집 상태 및 D-day 계산
3538
const recruitmentStatus = getRecruitmentStatus(recruitmentEndDate);
@@ -80,22 +83,37 @@ const ScrapListItem = ({
8083
});
8184
};
8285

86+
// S3 URL 체크 함수 추가
87+
const isValidS3Url = (url: string) => {
88+
return url.startsWith(S3_URL);
89+
};
90+
8391
return (
84-
<div className="relative h-[360px] w-[327px] overflow-hidden rounded-xl border border-grayscale-200 bg-white shadow-md transition-transform duration-300 hover:scale-[1.02] lg:h-[536px] lg:w-[477px]">
92+
<div className="relative h-auto w-[327px] overflow-hidden rounded-xl border border-grayscale-200 bg-white shadow-md transition-transform duration-300 hover:scale-[1.02] lg:w-[372px]">
8593
{/* 이미지 슬라이더 영역 */}
86-
<div className="relative h-[200px] overflow-hidden rounded-t-xl lg:h-[310px]">
87-
{/* 현재 이미지 */}
88-
{imageUrls[currentImageIndex] && (
89-
<Image
90-
src={imageUrls[currentImageIndex]}
91-
alt={`Recruit Image ${currentImageIndex + 1}`}
92-
fill
93-
className="object-cover transition-opacity duration-300"
94-
/>
94+
<div className="relative h-[200px] overflow-hidden rounded-t-xl lg:h-[240px]">
95+
{imageUrls[currentImageIndex] && !imageError ? (
96+
isValidS3Url(imageUrls[currentImageIndex]) ? (
97+
<Image
98+
src={imageUrls[currentImageIndex]}
99+
alt={`Recruit Image ${currentImageIndex + 1}`}
100+
fill
101+
className="object-cover transition-opacity duration-300"
102+
onError={() => setImageError(true)}
103+
/>
104+
) : (
105+
<div className="flex h-full w-full items-center justify-center bg-grayscale-100">
106+
<MdOutlineImage className="size-20 text-grayscale-400" />
107+
</div>
108+
)
109+
) : (
110+
<div className="flex h-full w-full items-center justify-center bg-grayscale-100">
111+
<MdOutlineImage className="size-20 text-grayscale-400" />
112+
</div>
95113
)}
96114

97-
{/* 이미지 인디케이터 */}
98-
{imageUrls.length > 1 && (
115+
{/* 이미지 인디케이터 - 유효한 이미지가 2개 이상이고 에러가 없을 때만 표시 */}
116+
{imageUrls.filter((url) => isValidS3Url(url)).length > 1 && !imageError && (
99117
<div className="absolute bottom-4 left-1/2 -translate-x-1/2">
100118
<Indicator
101119
imageCount={imageUrls.length}
@@ -107,7 +125,7 @@ const ScrapListItem = ({
107125
</div>
108126

109127
{/* 콘텐츠 영역 */}
110-
<div className="relative flex h-[140px] flex-col justify-between p-4 lg:h-[226px] lg:p-6">
128+
<div className="relative flex h-[140px] flex-col justify-between p-2 lg:h-[160px]">
111129
{/* 상단 영역 */}
112130
<div className="flex flex-col gap-4">
113131
{/* 상태 표시 영역 (공개여부, 모집상태, 날짜) */}
@@ -116,7 +134,7 @@ const ScrapListItem = ({
116134
<div className="flex items-center justify-between">
117135
<Chip label={isPublic ? "공개" : "비공개"} variant={isPublic ? "positive" : "negative"} />
118136
<Chip label={recruitmentStatus} variant={recruitmentStatus === "모집 중" ? "positive" : "negative"} />
119-
<span className="text-xs font-medium text-grayscale-500 md:inline">
137+
<span className="text-xs font-medium tracking-tighter text-grayscale-500 md:inline lg:text-sm">
120138
{formatRecruitDate(recruitmentStartDate, true)} ~ {formatRecruitDate(recruitmentEndDate, true)}
121139
</span>
122140
</div>
@@ -133,7 +151,7 @@ const ScrapListItem = ({
133151
{showDropdown && (
134152
<div className="absolute right-0 top-8 z-10 w-32 rounded-lg border border-grayscale-200 bg-white py-2 shadow-lg">
135153
<button
136-
className="w-full px-4 py-2 text-left text-sm hover:bg-primary-orange-100 disabled:opacity-50"
154+
className="w-full px-4 py-2 text-left text-sm hover:bg-primary-orange-100"
137155
onClick={handleFormApplication}
138156
>
139157
지원하기
@@ -151,11 +169,11 @@ const ScrapListItem = ({
151169
</div>
152170

153171
{/* 제목 */}
154-
<div className="text-grayscale-900 truncate text-base font-bold lg:text-lg">{title}</div>
172+
<div className="text-grayscale-900 truncate pl-2 text-base font-bold lg:text-lg">{title}</div>
155173
</div>
156174

157-
{/* 통계 정보 영역 - mt-auto 제거하고 부모 컨테이너에 justify-between 추가 */}
158-
<div className="text-grayscale-700 mt-4 flex h-[50px] items-center justify-between rounded-2xl border border-grayscale-100 p-2 text-sm lg:text-base">
175+
{/* 통계 정보 영역 */}
176+
<div className="text-grayscale-700 mt-4 flex h-[50px] items-center justify-between rounded-2xl border border-grayscale-100 text-sm lg:text-base">
159177
<div className="flex flex-1 items-center justify-center">
160178
<span className="font-medium">지원자 {applyCount}</span>
161179
</div>

src/app/components/chip/Chip.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,12 @@ interface ChipProps {
1919
const Chip: React.FC<ChipProps> = ({ label = "Label", variant, border, icon, textStyle = "" }: ChipProps) => {
2020
const wrapperStyle = "rounded flex items-center justify-center min-w-[60px] m-1";
2121
const paddingStyle = icon
22-
? "px-[10px] py-1 md:px-[14.5px] md:py-1 lg:px-[10px] lg:py-[6px]"
23-
: "px-2 py-1 md:px-[10px] lg:py-[6px] lg:px-3";
22+
? "px-[8px] py-1 md:px-[12px] md:py-1 lg:px-[8px] lg:py-[6px]"
23+
: "px-2 py-1 md:px-2 lg:py-[6px] lg:px-2";
2424
const variantStyle =
2525
variant === "positive" ? "bg-primary-orange-50 text-primary-orange-300" : "bg-line-100 text-grayscale-200";
2626
const baseTextStyle =
27-
"text-xs leading-[20px] md:leading-[24px] lg:text-base lg:leading-[26px] font-medium tracking-tight";
27+
"text-xs leading-[18px] md:leading-[20px] lg:text-sm lg:leading-[22px] font-medium tracking-tighter whitespace-nowrap";
2828
const borderStyle = border ? "border border-primary-orange-100" : "";
2929
const iconStyle = "flex items-center justify-center";
3030

src/app/stories/design-system/pages/albaList/page.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -100,9 +100,9 @@ const AlbaList: React.FC<AlbaListProps> = () => {
100100
</div>
101101

102102
{/* 메인 콘텐츠 영역 */}
103-
<div className="w-full pt-[224px]">
103+
<div className="w-full pt-[132px]">
104104
{/* 폼 만들기 버튼 - 고정 위치 */}
105-
<div className="fixed bottom-[28%] right-8 z-[9999] translate-y-1/2 md:right-12 lg:right-16 xl:right-20">
105+
<div className="fixed bottom-[50%] right-4 z-[9999] translate-y-1/2">
106106
<Link
107107
href="/addForm"
108108
className="flex items-center gap-2 rounded-lg bg-[#FFB800] px-4 py-3 text-base font-semibold text-white shadow-lg transition-all hover:bg-[#FFA800] md:px-6 md:text-lg"
@@ -117,8 +117,8 @@ const AlbaList: React.FC<AlbaListProps> = () => {
117117
<p className="text-grayscale-500">등록된 알바 공고가 없습니다.</p>
118118
</div>
119119
) : (
120-
<div className="mx-auto mt-4 w-full max-w-screen-2xl px-4 md:px-6 lg:px-8">
121-
<div className="flex flex-wrap items-center justify-center gap-6">
120+
<div className="mx-auto mt-4 w-full max-w-screen-xl px-3">
121+
<div className="flex flex-wrap justify-start gap-6">
122122
{items.map((form) => (
123123
<div key={form.id}>
124124
<AlbaListItem {...form} />

src/constants/config.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
export const API_URL = process.env.NEXT_PUBLIC_API_URL;
22

33
export const TEAM_NAME = process.env.NEXT_PUBLIC_TEAM_ID;
4+
5+
export const S3_URL = "https://sprint-fe-project.s3.ap-northeast-2.amazonaws.com";

src/utils/workDayFormatter.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
import { workDayOptions } from "@/constants/workDayOptions";
22

33
export const formatRecruitDate = (date: Date, isMd: boolean = false) => {
4+
// 유효한 Date 객체인지 확인
5+
if (!(date instanceof Date) || isNaN(date.getTime())) {
6+
return new Date().toLocaleDateString("ko-KR", { year: "numeric", month: "2-digit", day: "2-digit" });
7+
}
8+
49
const year = isMd ? date.getFullYear().toString() : date.getFullYear().toString().slice(2);
510
const month = String(date.getMonth() + 1).padStart(2, "0");
611
const day = String(date.getDate()).padStart(2, "0");

0 commit comments

Comments
 (0)