Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
e996eab
[#363] refactor: 최신 모집 공고관련 파 일,함수 이름 수정
zldn109 Nov 28, 2025
1b0fc55
[#363] feat: 모집공고 히스토리 카드 생성
zldn109 Jan 3, 2026
49465f4
[#363] feat: 모집히스토리 관련 api 연결
zldn109 Jan 3, 2026
1a93de9
[#363] feat: 히스토리 카드 클릭 시 상단 모집공고 변환되도록 구현
zldn109 Jan 4, 2026
b62f774
[#363] refactor: 모집공고 변경될 때 스크롤 상단으로 올리기
zldn109 Jan 4, 2026
77f9d5b
[#363] feat: 모집공고 리스트 페이지네이션 적용
zldn109 Jan 4, 2026
6860568
[#363] feat: 히스토리 카드 페이지네이션 반응형 처리
zldn109 Jan 4, 2026
e04f551
[#363] feat: 히스토리 카드 내에 제목이 2줄을 넘으면 말줄임으로 대체
zldn109 Jan 4, 2026
4457d23
[#363] feat: 전체모집공고 텍스트 추가
zldn109 Jan 4, 2026
8c3ce1c
[#363] refactor: 모집공고 상세 조회 쿼리 파라미터 처리 개선
zldn109 Jan 11, 2026
fe9b2ac
[#363] feat: 모집공고 히스토리를 페이지네이션방식에서 캐러샐 방식으로 구현
zldn109 Jan 11, 2026
c2561af
[#363] refactor: 모집공고히스토리 관련 네이밍 수정
zldn109 Jan 11, 2026
f3283a4
[#363] refactor: 함수명에 특정 도메인 붙여 수정
zldn109 Jan 11, 2026
cca26f0
[#363] refactor: qs에서 quertString으로 네이밍 수정
zldn109 Jan 11, 2026
6f9238e
[#363] fix: recruitForm의 url에 대해 반응형 시 너비 따라 줄바꿈 되도록 수정
zldn109 Jan 11, 2026
893ff49
[#363] fix: 동아리 상세페이지 헤더 글씨 깨짐 수정
zldn109 Jan 11, 2026
f5d68cc
[#363] refactor: 모바일버전에서의 히스토리 카드 섹션 관련 텍스트 크기 조정
zldn109 Jan 11, 2026
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
11 changes: 11 additions & 0 deletions public/pin.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
39 changes: 22 additions & 17 deletions src/entities/club-detail/ui/club-detail-tabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import Link from 'next/link';
import ClubRecruitWidget from '@/widgets/club-detail/ui/club-recruit-widget';
import ClubDescriptionWidget from '@/widgets/club-detail/ui/club-description-widget';
import ClubCommentsWidget from '@/widgets/club-detail/ui/club-comments-widget';
import { ClubRecruitments } from '@/views/club/model/type';

interface RecruitDetailViewProps {
title: string;
Expand All @@ -13,13 +14,16 @@ interface RecruitDetailViewProps {
recruitStart?: string;
recruitEnd?: string;
clubId: number;
id: number;
}

interface ClubDetailTabsProps {
activeTab: string;
isManageClub: boolean;
recruitData: RecruitDetailViewProps;
recruitHistories: ClubRecruitments[];
id: number;
rid: number;
}

const TABS = [
Expand All @@ -32,18 +36,15 @@ function ClubDetailTabs({
activeTab,
isManageClub,
recruitData,
recruitHistories,
id,
rid,
}: ClubDetailTabsProps) {
const getHref = (key: string) => {
switch (key) {
case 'about':
return `/club/${id}?tab=about`;
case 'comments':
return `/club/${id}?tab=comments`;
case 'recruit':
default:
return `/club/${id}`;
}
const queryString = new URLSearchParams();
queryString.set('rid', String(rid));
if (key !== 'recruit') queryString.set('tab', key);
return `/club/${id}?${queryString.toString()}`;
};

const renderContent = () => {
Expand All @@ -53,15 +54,19 @@ function ClubDetailTabs({
return (
<ClubRecruitWidget
isManageClub={isManageClub}
title={recruitData.title}
clubName={recruitData.clubName}
category={recruitData.category}
content={recruitData.content}
recruitForm={recruitData.recruitForm}
imageUrls={recruitData.imageUrls}
recruitStart={recruitData.recruitStart || ''}
recruitEnd={recruitData.recruitEnd || ''}
clubId={Number(id)}
recruitHistories={recruitHistories}
rid={rid}
recruitDetail={{
title: recruitData.title,
clubName: recruitData.clubName,
category: recruitData.category,
content: recruitData.content,
recruitForm: recruitData.recruitForm,
imageUrls: recruitData.imageUrls,
recruitStart: recruitData.recruitStart || '',
recruitEnd: recruitData.recruitEnd || '',
}}
/>
);
}
Expand Down
15 changes: 10 additions & 5 deletions src/entities/club-detail/ui/recruit-detail-header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,12 @@ function RecruitDetailHeader({
return (
<>
<header className="w-full cursor-default">
<div className="mb-4 flex flex-row items-center gap-5">
<div className="mb-4 flex flex-row items-center gap-2.5 lg:gap-5">
<ClickLogo logo={logo} title={title} />
<h1 className="text-xl font-bold lg:text-4xl">{title}</h1>
<p className="text-lg font-bold text-[#9C9C9C] lg:text-3xl">
<h1 className="text-xl font-bold whitespace-nowrap lg:text-4xl">
{title}
</h1>
<p className="text-lg font-bold whitespace-nowrap text-[#9C9C9C] lg:text-3xl">
<Link
href={`/recruit?category=${ClubCategoryToLabel[category].toLowerCase()}`}
>
Expand All @@ -46,12 +48,15 @@ function RecruitDetailHeader({
</p>
</div>
<div className="mb-4 flex flex-row items-center gap-4 lg:text-xl">
<RadiusTag status={status} className="lg:text-[16px]" />
<RadiusTag
status={status}
className="whitespace-nowrap lg:text-[16px]"
/>
<PeriodSection
startDate={startDate}
endDate={endDate}
decoration={false}
className="lg:text-lg"
className="whitespace-nowrap lg:text-lg"
/>
</div>
<div className="flex justify-between">
Expand Down
2 changes: 1 addition & 1 deletion src/entities/club-detail/ui/recruit-detail-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ function RecruitDetailView({
동아리 지원하러 가기: <br />
<a
href={recruitForm}
className="text-sm text-[#00E457] underline lg:text-lg"
className="text-sm break-all text-[#00E457] underline lg:text-lg"
target="_blank"
rel="noopener noreferrer"
>
Expand Down
30 changes: 30 additions & 0 deletions src/entities/club-detail/ui/recruit-history-card.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { ClubRecruitments } from '@/views/club/model/type';

interface RecruitHistoryCardProps {
recruitHistories: ClubRecruitments;
isSelected: boolean;
}

function RecruitHistoryCard({
recruitHistories,
isSelected,
}: RecruitHistoryCardProps) {
const formatDateDot = (iso: string) =>
iso ? iso.slice(0, 10).replaceAll('-', '.') : '';

return (
<div
data-selected={isSelected}
className="ring-primary-500 flex h-[132px] w-full max-w-[360px] cursor-pointer flex-col gap-3.5 rounded-2xl bg-[#F8F8F8] px-6 py-6 data-[selected=true]:ring lg:gap-1 lg:py-5"
>
<span className="text-sm text-[#474747] lg:text-lg">
{formatDateDot(recruitHistories.createdAt)}
</span>
<span className="line-clamp-2 font-semibold lg:text-lg">
{recruitHistories.title}
</span>
</div>
);
}

export default RecruitHistoryCard;
106 changes: 106 additions & 0 deletions src/entities/club-detail/ui/recruit-history-section.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import { ClubRecruitments } from '@/views/club/model/type';
import Link from 'next/link';
import { useState, useEffect } from 'react';
import RecruitHistoryCard from './recruit-history-card';

interface RecruitHistorySectionProps {
clubId: number;
recruitHistories: ClubRecruitments[];
selectedRid: number;
}

function useRecruitHistoryVisibleCardCount() {
const [cardCount, setCardCount] = useState(1);

useEffect(() => {
const update = () => {
const w = window.innerWidth;

if (w >= 1024) setCardCount(3);
else if (w >= 640) setCardCount(2);
else setCardCount(1);
};

update();
window.addEventListener('resize', update);
return () => window.removeEventListener('resize', update);
}, []);

return cardCount;
}

function RecruitHistorySection({
clubId,
recruitHistories,
selectedRid,
}: RecruitHistorySectionProps) {
const list = Array.isArray(recruitHistories) ? recruitHistories : [];
const visibleCardCount = useRecruitHistoryVisibleCardCount();

const [index, setIndex] = useState(0);
const maxIndex = Math.max(0, list.length - visibleCardCount);

const canPrev = index > 0;
const canNext = index < maxIndex;

const itemWidth = `${100 / visibleCardCount}%`;

return (
<>
<div className="mt-10 flex items-center gap-2 lg:gap-3">
<img src="/pin.svg" alt="pin" className="w-8" />
<span className="text-xl font-bold lg:text-2xl">전체 모집 공고</span>
</div>

<div className="mt-5 overflow-hidden pt-2">
<div
className="flex transition-transform duration-300 ease-out"
style={{
transform: `translateX(-${index * (100 / visibleCardCount)}%)`,
}}
>
{list.map((r) => {
const queryString = new URLSearchParams();
queryString.set('rid', String(r.id));
const href = `/club/${clubId}?${queryString.toString()}`;

return (
<div
key={r.id}
style={{ width: itemWidth }}
className="shrink-0 px-2"
>
<Link href={href}>
<RecruitHistoryCard
recruitHistories={r}
isSelected={selectedRid === r.id}
/>
</Link>
</div>
);
})}
</div>

<div className="mt-6 flex justify-center gap-6 text-sm">
<button
onClick={() => setIndex((v) => Math.max(0, v - 1))}
disabled={!canPrev}
className="disabled:opacity-40"
>
&lt; 이전
</button>

<button
onClick={() => setIndex((v) => Math.min(maxIndex, v + 1))}
disabled={!canNext}
className="disabled:opacity-40"
>
다음 &gt;
</button>
</div>
</div>
</>
);
}

export default RecruitHistorySection;
28 changes: 28 additions & 0 deletions src/views/club/api/getClubRecruitments.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import ErrorHandler from '@/shared/lib/error-message';
import { ApiResponse } from '@/shared/model/type';
import api from '@/shared/api/auth-api';
import { auth } from '@/auth';
import serverApi from '@/shared/api/server-api';
import { ClubRecruitmentsResponse } from '../model/type';

async function getClubRecruitments(clubId: number) {
const session = await auth();
try {
let response: ApiResponse<ClubRecruitmentsResponse>;
if (session?.accessToken) {
response = await api.get(`recruitments/club/${clubId}`).json();
} else {
response = await serverApi
.get(`recruitments/club/${clubId}`, {
cache: 'force-cache',
next: { revalidate: 3600 },
})
.json();
}
return { ok: true, data: response.data, status: 200 };
} catch (e) {
return ErrorHandler(e as Error);
}
}

export default getClubRecruitments;
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { auth } from '@/auth';
import serverApi from '@/shared/api/server-api';
import { RecruitmentDetail } from '../model/type';

async function getRecruitDetail(id: number) {
async function getRecentRecruitDetail(id: number) {
const session = await auth();
try {
let response: ApiResponse<RecruitmentDetail>;
Expand All @@ -25,4 +25,4 @@ async function getRecruitDetail(id: number) {
}
}

export default getRecruitDetail;
export default getRecentRecruitDetail;
28 changes: 28 additions & 0 deletions src/views/club/api/getRecruitDetail.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import ErrorHandler from '@/shared/lib/error-message';
import { ApiResponse } from '@/shared/model/type';
import api from '@/shared/api/auth-api';
import { auth } from '@/auth';
import serverApi from '@/shared/api/server-api';
import { RecruitmentDetail } from '../model/type';

async function getRecruitDetail(recruitmentId: number) {
const session = await auth();
try {
let response: ApiResponse<RecruitmentDetail>;
if (session?.accessToken) {
response = await api.get(`recruitments/${recruitmentId}`).json();
} else {
response = await serverApi
.get(`recruitments/${recruitmentId}`, {
cache: 'force-cache',
next: { revalidate: 3600 },
})
.json();
}
return { ok: true, data: response.data, status: 200 };
} catch (e) {
return ErrorHandler(e as Error);
}
}

export default getRecruitDetail;
17 changes: 17 additions & 0 deletions src/views/club/model/type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,25 @@ export interface RecruitmentDetail {
category: string;
clubName: string;
logo: string;
isAlwaysRecruiting: boolean;
}

export interface RecruitmentDetailResponse {
data: RecruitmentDetail;
}

export interface ClubRecruitments {
id: number;
title: string;
content: string;
recruitStart: string;
recruitEnd: string;
status: RecruitStatus;
createdAt: string;
firstImage?: string;
isAlwaysRecruiting: boolean;
}

export interface ClubRecruitmentsResponse {
recruitments: ClubRecruitments[];
}
Loading