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
222 changes: 215 additions & 7 deletions src/app/project/page.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,219 @@
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 안에 불리언(true/false) 값 저장
// // 1번째 함수 (재사용 가능한 UI 조각)
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="border-b border-gray-100 py-4 hover:bg-gray-50 transition-colors cursor-pointer w-full">
{/* [반응형 레이아웃 1] 모바일 카드형 (sm 미만에서 표시, sm 이상에서 hidden) */}
<div className="flex flex-col gap-3 sm:hidden px-4 w-full">
{/* 상단: 상태 및 포지션 배지 영역 */}
<div className="flex items-center gap-2 flex-wrap">
{/* [자바스크립트 문법] 템플릿 리터럴(` `)과 삼항 연산자를 사용한 동적 클래스 부여 */}
<span className={`rounded-full px-3 py-1 text-[10px] font-medium ${
isCompleted
? 'bg-gray-100 border border-gray-200 text-gray-700' // 완료 상태일 때
: 'border border-gray-300 text-gray-900' // 모집 중 상태일 때
}`}>
{status}
</span>
<span className="bg-gray-100 px-3 py-1 rounded-md text-[10px] font-semibold text-gray-700">
{position}
</span>
</div>

{/* 하단: 제목(왼쪽 정렬)과 작성자/시간(오른쪽 정렬) */}
<div className="flex justify-between items-end gap-4">
<div className="font-semibold text-gray-800 text-sm leading-snug break-keep">
{title}
{/* [자바스크립트 문법] && (단락 평가): comments가 있을 때만 💬 아이콘 렌더링 */}
{comments && <span className="text-blue-500 ml-1 text-xs font-normal">💬 {comments}</span>}
</div>

{/* 작성자 및 업로드 시간을 세로로 배치 */}
<div className="flex flex-col items-end flex-shrink-0 gap-1">
<span className="text-gray-600 text-[11px] font-medium">{author}</span>
<span className="text-gray-400 text-[10px] font-normal">{time}</span>
</div>
</div>
</div>

{/* [반응형 레이아웃 2] 태블릿/PC 그리드형 (sm 이상에서 표시, sm 미만에서 hidden) */}
<div className="hidden sm:grid sm:grid-cols-6 md:grid-cols-10 lg:grid-cols-13 text-center items-center">
<div className="col-span-1 md:col-span-2">
<span className={`rounded-full px-3 md:px-4 py-1 text-xs font-medium ${
isCompleted ? 'bg-gray-100 border border-gray-200 text-gray-700' : '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-3 md:col-span-4 lg:col-span-4 text-left md:text-center px-4 font-semibold text-gray-800 text-sm md:text-base">
{title}
{comments && <span className="text-blue-500 ml-2 text-xs font-normal">💬 {comments}</span>}
</div>
<div className="hidden md:block md:col-span-2 text-gray-500 text-sm">{author}</div>
<div className="hidden lg:block lg:col-span-2 text-gray-400 text-sm">{time}</div>
</div>
</div>
);
}

// [Next.js 문법] export default는 이 파일을 특정 주소(URL)로 접속했을 때 보여줄 '페이지'로 지정
export default function ProjectPage() {

// [자바스크립트/React 문법] 상태 관리 (State)
const [currentPage, setCurrentPage] = useState(1); // 현재 페이지네이션 번호
const [selectedPosition, setSelectedPosition] = useState('전체'); // 현재 선택된 포지션 필터
const [selectedStatus, setSelectedStatus] = useState('모집 중'); // 현재 선택된 모집 여부 필터

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

// --- (새로운 기능: 클릭 이벤트 핸들러) ---
// [자바스크립트 문법] 화살표 함수를 사용하여 클릭 시 상태 변경과 콘솔 기록을 동시에 수행
const handlePositionClick = (pos: string) => {
setSelectedPosition(pos);
console.log(`선택된 포지션: ${pos}`); // 브라우저 개발자 도구 콘솔에 기록
};

const handleStatusClick = (status: string) => {
setSelectedStatus(status);
console.log(`선택된 모집 상태: ${status}`);
};
// 2번째 함수 (전체 페이지)
return (
<div className="w-full bg-white min-h-screen">
{/* [메인 영역] pt-64: 모바일에서 길어진 헤더 높이만큼 상단 여백(Padding-Top)을 충분히 확보 */}
<main className="max-w-7xl mx-auto px-4 sm:px-6 pt-64 md:pt-32 py-12 bg-white min-h-screen font-sans">

{/* [1] 필터 구역 */}
<div className="mb-8 p-4 sm:p-8 bg-white rounded-xl">
<div className="flex flex-col gap-6 md:gap-8">

{/* 1-1. 구인 포지션 필터 (모바일에서는 세로 레이아웃) */}
<div className="flex flex-col md:flex-row md:items-center gap-3 md:gap-6">
<div className="w-auto md:w-28 font-bold text-gray-800 text-base md:text-lg whitespace-nowrap">
구인 포지션
</div>
<div className="flex gap-2 flex-wrap">
{/* [자바스크립트 문법] .map()을 사용하여 배열 데이터만큼 버튼 생성 */}
{positions.map((pos) => (
<button
key={pos}
onClick={() => handlePositionClick(pos)}
className={`px-4 py-1.5 md:px-6 md:py-2 rounded-full text-xs md: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 flex-col md:flex-row md:items-center gap-3 md:gap-6">
<div className="w-auto md:w-28 font-bold text-gray-800 text-base md:text-lg whitespace-nowrap">
모집 여부
</div>
<div className="flex gap-2">
{/* [자바스크립트 문법] 즉석에서 생성한 배열(['모집 중', '완료'])로 map 실행 */}
{['모집 중', '완료'].map((st) => (
<button
key={st}
onClick={() => handleStatusClick(st)}
className={`px-6 py-1.5 md:px-8 md:py-2 rounded-full text-xs md: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-0 sm:p-6 bg-white rounded-xl">
{/* 리스트 헤더: sm(640px) 이상 화면에서만 보이도록 설정 */}
<div className="hidden sm:grid sm:grid-cols-6 md:grid-cols-10 lg:grid-cols-13 border-b-2 border-black pb-4 mb-2 text-center font-bold text-gray-700 text-sm md:text-base">
<div className="col-span-1 md:col-span-2">여부</div>
<div className="col-span-2">포지션</div>
<div className="col-span-3 md:col-span-4 lg:col-span-4 text-left md:text-center">프로젝트 모집 글</div>
<div className="hidden md:block md:col-span-2">작성자</div>
<div className="hidden lg:block lg:col-span-2">업로드 시간</div>
</div>

{/* [자바스크립트/React] 사용자 정의 컴포넌트 ProjectRow에 데이터를 전달(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="VR 게임 개발 같이하실 분" author="OOO" time="12시간 전" />
</div>

{/* [3] 페이지네이션 */}
<div className="flex justify-center items-center gap-2 mt-12 pb-10">
<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);
console.log(`페이지 이동: ${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>
);
}