Skip to content
Open
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
208 changes: 201 additions & 7 deletions src/app/project/page.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,205 @@
export default function ProjectPage() {
// [Next.js 문법] 'use client'는 이 컴포넌트가 브라우저에서 상호작용(useState, 클릭 등)을 한다는 것을 Next.js에 알림
'use client';

import { useState } from 'react';

// --- (7주차 추가: 공통 컴포넌트 추출 - 프로젝트 리스트 아이템) ---
// [자바스크립트/React] 독립적인 UI 단위를 함수로 만든 '컴포넌트' 파트
function ProjectRow({
status,
position,
title,
author,
time,
comments
}: {
status: string;
position: string;
title: string;
author: string;
time: string;
comments?: number;
}) {
// [자바스크립트 문법] 변수 선언 및 조건식(비교 연산)
const isCompleted = status === '완료'; //isCompleted 안에 불리언 값 저장

return (
<main className="flex min-h-[50vh] flex-col items-center justify-center gap-4 bg-gray-0 px-6 py-16 text-center">
<h1 className="text-3xl font-semibold text-gray-900">프로젝트 모집</h1>
<p className="max-w-2xl text-base text-gray-600">
팀 프로젝트 모집 게시판이 준비 중입니다. 곧 더 많은 콘텐츠를 확인할 수 있어요.
</p>
</main>
<div className="grid grid-cols-13 border-b border-gray-100 py-5 text-center items-center hover:bg-gray-50 transition-colors cursor-pointer group">
<div className="col-span-2">
{/* [자바스크립트 문법] 템플릿 리터럴(` `)과 삼항 연산자를 사용하여 조건에 따라 테일윈드 클래스(문자열)를 동적으로 변경. */}
<span className={`rounded-full px-4 py-1 text-xs font-medium ${ // 템플릿 리터널 -> 자바스크립트안에 테일윈드가 들어감???
isCompleted
? 'bg-gray-100 border border-gray-200 text-gray-700 font-semibold' // 완료된 상태 (참일 때)
: 'border border-gray-300 text-gray-900' // 모집 중 상태 (거짓일 때)
}`}>
{status}
</span>
</div>
<div className="col-span-2">
<span className="bg-gray-100 px-3 py-1 rounded-md text-xs font-semibold text-gray-700">
{position}
</span>
</div>
<div className="col-span-4 text-center px-4 font-semibold text-gray-800">
{title}
{/* [자바스크립트 문법] && (단락 평가) 문법: comments가 존재할 때만 뒤의 JSX를 렌더링. */}
{comments && <span className="text-blue-500 ml-2 text-xs font-normal">💬 {comments}</span>}
{/* (&& 단락평가 문법) A && B: A가 **참(true)**이면 B를 반환 A && B: A가 거짓(false)이면 A에서 멈춤.*/}
</div>
<div className="col-span-2 text-gray-500 text-sm">{author}</div>
<div className="col-span-2 text-gray-400 text-sm">{time}</div>
</div>
);
}

// [Next.js 문법] export default는 이 파일을 특정 주소(URL)로 접속했을 때 보여줄 '페이지'로 지정
export default function ProjectPage() { // (7주차 수정: 페이지 이름 의미에 맞게 변경)

// [자바스크립트/React 문법] 상태 관리 (State)
const [currentPage, setCurrentPage] = useState(1); // 페이지네이션 상태
// --- (7주차 추가: 필터 상태 관리) ---
const [selectedPosition, setSelectedPosition] = useState('전체'); // (7주차 추가: 필터 상태)
const [selectedStatus, setSelectedStatus] = useState('모집 중'); // (7주차 추가: 상태 필터)

// [자바스크립트 문법] 상수 데이터 배열 정의
const positions = ['전체', '프론트엔드', '백엔드', '개발', '디자인', '기획', '기타'];
const totalPages = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

return (
<div className="w-full bg-white min-h-screen">
<main className="max-w-7xl mx-auto px-6 pt-24 py-12 bg-white min-h-screen font-sans">

{/* [1] 필터 구역 (컴포넌트화 전, 상태 연동 준비) */}
<div className="mb-8 p-8 bg-white rounded-xl">
<div className="flex flex-col gap-8">
{/* 1-1. 구인 포지션 필터 */}
<div className="flex items-center gap-6">
<div className="w-28 font-bold text-gray-800 text-lg">구인 포지션</div>
<div className="flex gap-3 flex-wrap">
{/* [자바스크립트 문법] .map()을 사용한 리스트 렌더링 */}
{positions.map((pos) => (
<button
key={pos}
onClick={() => setSelectedPosition(pos)} // (7주차 추가: 클릭 이벤트 - JS 익명 함수)
className={`px-6 py-2 rounded-full text-sm font-medium transition-all ${
selectedPosition === pos
? 'bg-blue-600 text-white shadow-sm'
: 'border border-gray-200 text-gray-700 hover:bg-gray-50'
}`}
>
{pos}
</button>
))}
</div>
</div>

{/* 1-2. 모집 여부 필터 */}
<div className="flex items-center gap-6">
<div className="w-28 font-bold text-gray-800 text-lg">모집 여부</div>
<div className="flex gap-3 flex-wrap">
{/* [자바스크립트 문법] 즉석 배열(['모집 중', '완료'])을 생성하여 map 실행 */}
{['모집 중', '완료'].map((st) => (
<button
key={st}
onClick={() => setSelectedStatus(st)}
className={`px-6 py-2 rounded-full text-sm font-medium transition-all ${
selectedStatus === st
? 'bg-blue-600 text-white shadow-sm'
: 'border border-gray-200 text-gray-700 hover:bg-gray-50'
}`}
>
{st}
</button>
))}
</div>
</div>
</div>
</div>

{/* [2] 리스트 구역 (컴포넌트 적용) */}
<div className="mb-8 p-6 bg-white rounded-xl">
<div className="grid grid-cols-13 border-b-2 border-black pb-4 mb-2 text-center font-bold text-gray-700 text-base">
<div className="col-span-2">모집 여부</div>
<div className="col-span-2">구인 포지션</div>
<div className="col-span-4">프로젝트 모집 글</div>
<div className="col-span-2">작성자</div>
<div className="col-span-2">업로드 시간</div>
</div>

{/* (7주차 수정: 개별 행을 ProjectRow 컴포넌트로 대체) */}
{/* [자바스크립트/React] 사용자 정의 컴포넌트에 데이터를 전달(Props)하는 파트 */}
<ProjectRow
status="모집 중"
position="프론트"
title=" 프로젝트 모집합니다~"
author="OOO"
time="6시간 전"
comments={1}
/>
<ProjectRow
status="완료"
position="알고리즘"
title=" 공모전 함께하실 분!"
author="OOO"
time="12시간 전"
/>
<ProjectRow
status="모집 중"
position="서버"
title=" "
author="OOO"
time="12시간 전"
/>
<ProjectRow
status="모집 중"
position="전부"
title=" "
author="OOO"
time="12시간 전"
/>
<ProjectRow
status="모집 중"
position="풀스택"
title=" "
author="OOO"
time="12시간 전"
/>
<ProjectRow
status="모집 중"
position="유니티"
title=" "
author="OOO"
time="12시간 전"
/>
</div>

{/* [3] 페이지네이션 */}
<div className="flex justify-center items-center gap-2 mt-12">
<button
onClick={() => setCurrentPage(p => Math.max(1, p - 1))}
className="p-2 text-gray-400 hover:text-gray-600"
>
{"<"}
</button>
{totalPages.map((num) => (
<button
key={num}
onClick={() => setCurrentPage(num)}
className={`w-9 h-9 flex items-center justify-center rounded-md text-sm font-bold transition-all ${
currentPage === num ? 'bg-gray-200 text-gray-800' : 'text-gray-400 hover:bg-gray-50'
}`}
>
{num}
</button>
))}
<button
onClick={() => setCurrentPage(p => Math.min(totalPages.length, p + 1))}
className="p-2 text-gray-400 hover:text-gray-600"
>
{">"}
</button>
</div>
</main>
</div>
);
}
163 changes: 156 additions & 7 deletions src/app/study/page.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,160 @@
export default function StudyPage() {
// [Next.js 문법] 'use client'는 클라이언트 사이드 렌더링을 명시하는 Next.js 전용 지시어
'use client';

import { useState } from 'react';

// 1. 카드 컴포넌트 (일반 React/JS 함수형 컴포넌트)
function StudyCard({ number }: { number: number }) {
return (
<main className="flex min-h-[50vh] flex-col items-center justify-center gap-4 bg-gray-0 px-6 py-16 text-center">
<h1 className="text-3xl font-semibold text-gray-900">스터디 모집</h1>
<p className="max-w-2xl text-base text-gray-600">
진행 중인 스터디 모집 공지와 신청 폼이 곧 업데이트될 예정입니다.
</p>
</main>
<div className="bg-white rounded-2xl border border-gray-200 shadow-sm flex flex-col hover:shadow-md transition-shadow cursor-pointer overflow-hidden">
<div className="p-6 flex flex-col gap-4">
<div className="flex justify-between items-center">
<span className="bg-emerald-50 text-emerald-500 px-2 py-1 rounded text-xs font-bold">모집 중</span>
<span className="text-gray-400 text-xs font-light">🕑6시간 전</span>
</div>
<div className="flex flex-col gap-9">
<h3 className="text-lg font-bold text-gray-900 leading-snug">웹개발 스터디 모집합니다~</h3>
<div><span className="bg-yellow-100 text-yellow-700 px-2 py-1 rounded text-[10px] font-semibold">C++</span></div>
</div>
</div>
<div className="bg-gray-50 px-6 py-4 border-t border-gray-100 flex justify-between items-center">
<div className="flex items-center gap-2">
<div className="w-6 h-6 bg-gray-200 rounded-full"></div>
<span className="text-sm text-gray-700 font-medium">aBCDFEFGOL</span>
</div>
<div className="flex gap-3 text-xs text-gray-400"><span>👁️ 122</span><span>💬 333</span></div>
</div>
</div>
);
}

// [Next.js 문법] export default 함수는 해당 파일의 대표 페이지가 됩니다. (App Router 라우팅)
export default function StudyPage() { //라우팅 경로에 맞게 함수명 변경

// [자바스크립트/React 문법] 상태 관리를 위한 Hook 사용
const [selected, setSelected] = useState('전체');
const [status, setStatus] = useState('모집 중');
const [currentPage, setCurrentPage] = useState(1);

// [자바스크립트 문법] 변수 및 배열 선언
const categories = ['전체', 'C', 'Python', 'Java', '알고리즘', "기타"];
const totalPages = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

// --- (7주차 추가 시작 : 콘솔로그 - 자바스크립트 로직 파트) ---
// 카테고리 클릭 시 실행될 함수
const handleCategoryClick = (category: string) => {
setSelected(category);
console.log(`선택된 카테고리: ${category}`); // 개별적인 이벤트 로그
};

// 모집 상태 클릭 시 실행될 함수
const handleStatusClick = (statusName: string) => {
setStatus(statusName);
// 모집 중이면 true, 모집 완료면 false
const isRecruiting = statusName === '모집 중'; // 왼쪽 값과 오른쪽 값을 비교하여 결과를 무조건 불리언

// false(모집 완료)일 때만 콘솔에 찍기
if (!isRecruiting) {
console.log(`모집 상태: ${isRecruiting}`);
}
};
// --- (7주차 추가 끝 : 콘솔로그) ---

return (
// (7주차 수정) 전체 화면 너비를 차지하는 배경 레이어 추가 -> 사이드 검둥이 제거
<div className="w-full bg-white min-h-screen">
<main className="max-w-7xl mx-auto px-6 pt-24 py-12 bg-white min-h-screen">

{/* 1번 네모: 제목 */}
<div className="mb-8 p-6">
<h1 className="text-3xl font-bold text-gray-900">스터디 모집 공고</h1>
</div>

{/* 2번 네모: 필터 구역 (7주차 수정: 반응형 레이아웃 적용) */}
<div className="flex flex-col md:flex-row justify-between items-start md:items-center gap-6 mb-10">
{/* !!!!(7주차 수정) flex-col로 세로 정렬하되, md: 768px 이상에서만 가로(row) 정렬, items-start로 왼쪽 정렬 */}

{/* 카테고리 버튼 구역 (7주차 수정: 줄바꿈 허용) */}
<div className="flex flex-wrap gap-2">
{/* (7주차 추가) flex-wrap을 넣어 화면이 좁아지면 버튼이 다음 줄로 넘어가게 함 */}
{/* [자바스크립트 문법] .map()을 사용하여 배열 데이터를 JSX 리스트로 변환 JSX 안의 {} 사용*/}
{categories.map((category) => (
<button
key={category}
onClick={() => handleCategoryClick(category)}
className={`px-5 py-2 rounded-full text-sm font-medium transition-all ${
selected === category
? 'bg-blue-600 text-white shadow-sm'
: 'bg-white text-gray-600 border border-gray-200 hover:bg-gray-50'
}`}
>
{category}
</button>
))}
</div>

{/* 모집 상태 버튼 구역 (7주차 수정: 모바일 환경에서는 숨기고 데스크톱에서만 표시) */}
<div className="hidden md:flex bg-gray-100 p-1 rounded-full border border-gray-200">
{/* (7주차 수정) 'hidden'으로 기본 숨김 처리, 'md:flex'로 768px 이상에서만 나타나게 함 -> 모바일 환경 우선*/}
<button
onClick={() => handleStatusClick('모집 중')}
className={`px-4 py-1.5 rounded-full text-xs font-bold transition-all ${
status === '모집 중' ? 'bg-white text-gray-900 shadow-sm' : 'text-gray-400 hover:text-gray-600'
}`}
>
모집 중
</button>
<button
onClick={() => handleStatusClick('모집 완료')}
className={`px-4 py-1.5 rounded-full text-xs font-bold transition-all ${
status === '모집 완료' ? 'bg-white text-gray-900 shadow-sm' : 'text-gray-400 hover:text-gray-600'
}`}
>
모집 완료
</button>
</div>
</div>

{/* 3번 네모: 카드 그리드 */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"> {/*카드의 반응형을 담당하는 핵심 코드*/}
{/* [자바스크립트 문법] 빈 배열을 생성하여 반복 렌더링 수행 */}
{[...Array(9)].map((_, i) => (
<StudyCard key={i} number={i + 1} />
))}
</div>

{/* 4번 네모: 페이지네이션 */}
<div className="flex justify-center items-center gap-2 mt-12">
{/* [자바스크립트 문법] 화살표 함수와 Math 객체를 활용한 로직 처리 */}
<button
onClick={() => setCurrentPage(p => Math.max(1, p - 1))}
className="p-2 text-gray-400 hover:text-gray-600"
>
{"<"}
</button>

{totalPages.map((num) => (
<button
key={num}
onClick={() => setCurrentPage(num)}
className={`w-8 h-8 flex items-center justify-center rounded-md text-sm font-medium transition-all ${
currentPage === num
? 'bg-gray-200 text-gray-700 shadow-sm'
: 'text-gray-400 hover:bg-gray-50'
}`}
>
{num}
</button>
))}

<button
onClick={() => setCurrentPage(p => Math.min(totalPages.length, p + 1))}
className="p-2 text-gray-400 hover:text-gray-600"
>
{">"}
</button>
</div>
</main>
</div>
);
}