From 6df10d960dca3ab7c83b25701cf4dc20617c7a08 Mon Sep 17 00:00:00 2001 From: Asterisk-_ <101379724+Asterisk0707@users.noreply.github.com> Date: Wed, 24 Dec 2025 05:42:38 +0900 Subject: [PATCH 1/4] =?UTF-8?q?feat:=20=EC=8A=A4=ED=84=B0=EB=94=94=20?= =?UTF-8?q?=EB=AA=A8=EC=A7=91,=20=ED=94=84=EB=A1=9C=EC=A0=9D=ED=8A=B8=20?= =?UTF-8?q?=EB=AA=A8=EC=A7=91=20=ED=99=94=EB=A9=B4=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 스터디 모집 화면 中 프로젝트 모집 화면 完 --- src/app/project/page.tsx | 151 ++++++++++++++++++++++++++++++++++++--- src/app/study/page.tsx | 128 +++++++++++++++++++++++++++++++-- 2 files changed, 264 insertions(+), 15 deletions(-) diff --git a/src/app/project/page.tsx b/src/app/project/page.tsx index b7b7ed9..7509b55 100644 --- a/src/app/project/page.tsx +++ b/src/app/project/page.tsx @@ -1,11 +1,146 @@ -export default function ProjectPage() { +'use client'; +import { useState } from 'react'; + +export default function StudyPage() { + const [currentPage, setCurrentPage] = useState(1); // 현재 페이지 상태 + const totalPages = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; // 페이지 번호 목록 + return ( -
-

프로젝트 모집

-

- 팀 프로젝트 모집 게시판이 준비 중입니다. 곧 더 많은 콘텐츠를 확인할 수 있어요. -

+ /* [전체 컨테이너] */ +
+ {/* [1] 2번 네모: 필터 구역 */} +
+
+ {/* 1-1. 구인 포지션 필터 */} +
+
구인 포지션
+
+ + + + + + + +
+
+ + {/* 1-2. 모집 여부 필터 */} +
+
모집 여부
+
+ + +
+
+
+
+ + {/* [2] 3번 네모: 카드(리스트) 구역 */} +
+ + {/* 테이블 제목 줄 */} +
+
모집 여부
+
구인 포지션
+
프로젝트 모집 글
+
작성자
+
업로드 시간
+
+ + {/* 리스트 줄 - 예시 1 */} +
+
+ + 모집 중 + +
+
+ + 프론트 + +
+
+ && 프로젝트 모집합니다~ +
+
OOO
+
6시간 전
+ 💬 1 + +
+ + {/* 리스트 줄 - 예시 2 */} +
+
+ + 완료 + +
+
+ + 알고리즘 + +
+
+ && 공모전 함께하실 분! +
+
OOO
+
12시간 전
+
+
+ + {/* [3] 4번 네모: 페이지네이션 */} +
+ {/* 이전 버튼 */} + + + {/* 숫자 버튼들 */} + {totalPages.map((num) => ( + + ))} + + {/* 다음 버튼 */} + +
+
); -} - +} \ No newline at end of file diff --git a/src/app/study/page.tsx b/src/app/study/page.tsx index 09438d2..f5ebe19 100644 --- a/src/app/study/page.tsx +++ b/src/app/study/page.tsx @@ -1,11 +1,125 @@ -export default function StudyPage() { +'use client'; + +import { useState } from 'react'; + +// 1. 카드 컴포넌트 +function StudyCard({ number }: { number: number }) { return ( -
-

스터디 모집

-

- 진행 중인 스터디 모집 공지와 신청 폼이 곧 업데이트될 예정입니다. -

-
+
+
+
+ 모집 중 + 🕑6시간 전 +
+
+

웹개발 스터디 모집합니다~

+
C++
+
+
+
+
+
+ aBCDFEFGOL +
+
👁️ 122💬 333
+
+
); } +// 2. 메인 페이지setStatus + +export default function StudyPage() { + 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]; // 페이지 번호 목록 + + return ( +
+ + {/* 1번 네모: 제목 */} +
+

스터디 모집 공고

+
+ + {/* 2번 네모: 필터 구역 */} +
+
+ {categories.map((category) => ( // 카테고리 버튼 */} + + ))} +
+ +
+ + +
+
+ + {/* 3번 네모: 카드 그리드 */} +
+ {[...Array(9)].map((_, i) => ( // 9개의 카드 생성 + // 고유 키와 번호 전달 + ))} +
+ + {/* 4번 네모: 페이지네이션 */} +
+ + + {totalPages.map((num) => ( + + ))} + + +
+
+ ); +} \ No newline at end of file From b993b833d9d3dfab673f53544ba8b6b0af88f1d6 Mon Sep 17 00:00:00 2001 From: Asterisk-_ Date: Thu, 1 Jan 2026 13:47:23 +0900 Subject: [PATCH 2/4] =?UTF-8?q?feat:=20=EC=8A=A4=ED=84=B0=EB=94=94=20?= =?UTF-8?q?=EB=AA=A8=EC=A7=91=20=EA=B2=8C=EC=8B=9C=ED=8C=90=20=EB=A0=88?= =?UTF-8?q?=EC=9D=B4=EC=95=84=EC=9B=83=20=EA=B0=9C=EC=84=A0=20=EB=B0=8F=20?= =?UTF-8?q?=ED=94=84=EB=A1=9C=EC=A0=9D=ED=8A=B8=20=EB=AA=A8=EC=A7=91=20?= =?UTF-8?q?=EA=B2=8C=EC=8B=9C=ED=8C=90=20=EC=BB=B4=ED=8F=AC=EB=84=8C?= =?UTF-8?q?=ED=8A=B8=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/project/page.tsx | 235 ++++++++++++++++++++++++--------------- src/app/study/page.tsx | 91 ++++++++++----- 2 files changed, 210 insertions(+), 116 deletions(-) diff --git a/src/app/project/page.tsx b/src/app/project/page.tsx index 7509b55..16ff491 100644 --- a/src/app/project/page.tsx +++ b/src/app/project/page.tsx @@ -1,63 +1,123 @@ +// [Next.js 문법] 'use client'는 이 컴포넌트가 브라우저에서 상호작용(useState, 클릭 등)을 한다는 것을 Next.js에 알림 'use client'; + import { useState } from 'react'; -export default function StudyPage() { - const [currentPage, setCurrentPage] = useState(1); // 현재 페이지 상태 - const totalPages = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; // 페이지 번호 목록 +// --- (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 ( - /* [전체 컨테이너] */ +
+
+ {/* [자바스크립트 문법] 템플릿 리터럴(` `)과 삼항 연산자를 사용하여 조건에 따라 테일윈드 클래스(문자열)를 동적으로 변경. */} + 자바스크립트안에 테일윈드가 들어감??? + isCompleted + ? 'bg-gray-100 border border-gray-200 text-gray-700 font-semibold' // 완료된 상태 (참일 때) + : 'border border-gray-300 text-gray-900' // 모집 중 상태 (거짓일 때) + }`}> + {status} + +
+
+ + {position} + +
+
+ {title} + {/* [자바스크립트 문법] && (단락 평가) 문법: comments가 존재할 때만 뒤의 JSX를 렌더링. */} + {comments && 💬 {comments}} + {/* (&& 단락평가 문법) A && B: A가 **참(true)**이면 B를 반환 A && B: A가 거짓(false)이면 A에서 멈춤.*/} +
+
{author}
+
{time}
+
+ ); +} + +// [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 ( +
- {/* [1] 2번 네모: 필터 구역 */} -
+ + {/* [1] 필터 구역 (컴포넌트화 전, 상태 연동 준비) */} +
{/* 1-1. 구인 포지션 필터 */}
구인 포지션
- - - - - - - + {/* [자바스크립트 문법] .map()을 사용한 리스트 렌더링 */} + {positions.map((pos) => ( + + ))}
{/* 1-2. 모집 여부 필터 */}
모집 여부
-
- - +
+ {/* [자바스크립트 문법] 즉석 배열(['모집 중', '완료'])을 생성하여 map 실행 */} + {['모집 중', '완료'].map((st) => ( + + ))}
- {/* [2] 3번 네모: 카드(리스트) 구역 */} + {/* [2] 리스트 구역 (컴포넌트 적용) */}
- - {/* 테이블 제목 줄 */}
모집 여부
구인 포지션
@@ -66,81 +126,80 @@ export default function StudyPage() {
업로드 시간
- {/* 리스트 줄 - 예시 1 */} -
-
- - 모집 중 - -
-
- - 프론트 - -
-
- && 프로젝트 모집합니다~ -
-
OOO
-
6시간 전
- 💬 1 - -
- - {/* 리스트 줄 - 예시 2 */} -
-
- - 완료 - -
-
- - 알고리즘 - -
-
- && 공모전 함께하실 분! -
-
OOO
-
12시간 전
-
+ {/* (7주차 수정: 개별 행을 ProjectRow 컴포넌트로 대체) */} + {/* [자바스크립트/React] 사용자 정의 컴포넌트에 데이터를 전달(Props)하는 파트 */} + + + + + +
- {/* [3] 4번 네모: 페이지네이션 */} + {/* [3] 페이지네이션 */}
- {/* 이전 버튼 */} - - {/* 숫자 버튼들 */} {totalPages.map((num) => ( ))} - - {/* 다음 버튼 */}
-
+
); } \ No newline at end of file diff --git a/src/app/study/page.tsx b/src/app/study/page.tsx index f5ebe19..e49ee1c 100644 --- a/src/app/study/page.tsx +++ b/src/app/study/page.tsx @@ -1,8 +1,9 @@ +// [Next.js 문법] 'use client'는 클라이언트 사이드 렌더링을 명시하는 Next.js 전용 지시어 'use client'; import { useState } from 'react'; -// 1. 카드 컴포넌트 +// 1. 카드 컴포넌트 (일반 React/JS 함수형 컴포넌트) function StudyCard({ number }: { number: number }) { return (
@@ -27,17 +28,41 @@ function StudyCard({ number }: { number: number }) { ); } -// 2. 메인 페이지setStatus +// [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); -export default function StudyPage() { - 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]; - 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주차 수정) 전체 화면 너비를 차지하는 배경 레이어 추가 -> 사이드 검둥이 제거 +
{/* 1번 네모: 제목 */} @@ -45,17 +70,22 @@ export default function StudyPage() {

스터디 모집 공고

- {/* 2번 네모: 필터 구역 */} -
-
- {categories.map((category) => ( // 카테고리 버튼 */} + {/* 2번 네모: 필터 구역 (7주차 수정: 반응형 레이아웃 적용) */} +
+ {/* !!!!(7주차 수정) flex-col로 세로 정렬하되, md: 768px 이상에서만 가로(row) 정렬, items-start로 왼쪽 정렬 */} + + {/* 카테고리 버튼 구역 (7주차 수정: 줄바꿈 허용) */} +
+ {/* (7주차 추가) flex-wrap을 넣어 화면이 좁아지면 버튼이 다음 줄로 넘어가게 함 */} + {/* [자바스크립트 문법] .map()을 사용하여 배열 데이터를 JSX 리스트로 변환 JSX 안의 {} 사용*/} + {categories.map((category) => (
-
+ {/* 모집 상태 버튼 구역 (7주차 수정: 모바일 환경에서는 숨기고 데스크톱에서만 표시) */} +
+ {/* (7주차 수정) 'hidden'으로 기본 숨김 처리, 'md:flex'로 768px 이상에서만 나타나게 함 -> 모바일 환경 우선*/}
{/* 3번 네모: 카드 그리드 */} -
- {[...Array(9)].map((_, i) => ( // 9개의 카드 생성 - // 고유 키와 번호 전달 +
{/*카드의 반응형을 담당하는 핵심 코드*/} + {/* [자바스크립트 문법] 빈 배열을 생성하여 반복 렌더링 수행 */} + {[...Array(9)].map((_, i) => ( + ))}
{/* 4번 네모: 페이지네이션 */}
+ {/* [자바스크립트 문법] 화살표 함수와 Math 객체를 활용한 로직 처리 */}
+ ); } \ No newline at end of file From fbac81400fb7af28417cb48316470358a08a3eeb Mon Sep 17 00:00:00 2001 From: Asterisk-_ Date: Mon, 5 Jan 2026 01:47:05 +0900 Subject: [PATCH 3/4] =?UTF-8?q?feat:=20=EA=B2=8C=EC=8B=9C=ED=8C=90=20?= =?UTF-8?q?=EB=A0=88=EC=9D=B4=EC=95=84=EC=9B=83=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 반응형과 콘솔 로그 추가 --- src/app/project/page.tsx | 322 ++++++++++++++++++++------------------- 1 file changed, 168 insertions(+), 154 deletions(-) diff --git a/src/app/project/page.tsx b/src/app/project/page.tsx index 16ff491..f691bfe 100644 --- a/src/app/project/page.tsx +++ b/src/app/project/page.tsx @@ -21,185 +21,199 @@ function ProjectRow({ comments?: number; }) { // [자바스크립트 문법] 변수 선언 및 조건식(비교 연산) - const isCompleted = status === '완료'; //isCompleted 안에 불리언 값 저장 - + const isCompleted = status === '완료'; // isCompleted 안에 불리언(true/false) 값 저장 + // // 1번째 함수 (재사용 가능한 UI 조각) return ( -
-
- {/* [자바스크립트 문법] 템플릿 리터럴(` `)과 삼항 연산자를 사용하여 조건에 따라 테일윈드 클래스(문자열)를 동적으로 변경. */} - 자바스크립트안에 테일윈드가 들어감??? - isCompleted - ? 'bg-gray-100 border border-gray-200 text-gray-700 font-semibold' // 완료된 상태 (참일 때) - : 'border border-gray-300 text-gray-900' // 모집 중 상태 (거짓일 때) - }`}> - {status} - -
-
- - {position} - +
+ {/* [반응형 레이아웃 1] 모바일 카드형 (sm 미만에서 표시, sm 이상에서 hidden) */} +
+ {/* 상단: 상태 및 포지션 배지 영역 */} +
+ {/* [자바스크립트 문법] 템플릿 리터럴(` `)과 삼항 연산자를 사용한 동적 클래스 부여 */} + + {status} + + + {position} + +
+ + {/* 하단: 제목(왼쪽 정렬)과 작성자/시간(오른쪽 정렬) */} +
+
+ {title} + {/* [자바스크립트 문법] && (단락 평가): comments가 있을 때만 💬 아이콘 렌더링 */} + {comments && 💬 {comments}} +
+ + {/* 작성자 및 업로드 시간을 세로로 배치 */} +
+ {author} + {time} +
+
-
- {title} - {/* [자바스크립트 문법] && (단락 평가) 문법: comments가 존재할 때만 뒤의 JSX를 렌더링. */} - {comments && 💬 {comments}} - {/* (&& 단락평가 문법) A && B: A가 **참(true)**이면 B를 반환 A && B: A가 거짓(false)이면 A에서 멈춤.*/} + + {/* [반응형 레이아웃 2] 태블릿/PC 그리드형 (sm 이상에서 표시, sm 미만에서 hidden) */} +
+
+ + {status} + +
+
+ + {position} + +
+
+ {title} + {comments && 💬 {comments}} +
+
{author}
+
{time}
-
{author}
-
{time}
); } // [Next.js 문법] export default는 이 파일을 특정 주소(URL)로 접속했을 때 보여줄 '페이지'로 지정 -export default function ProjectPage() { // (7주차 수정: 페이지 이름 의미에 맞게 변경) +export default function ProjectPage() { // [자바스크립트/React 문법] 상태 관리 (State) - const [currentPage, setCurrentPage] = useState(1); // 페이지네이션 상태 - // --- (7주차 추가: 필터 상태 관리) --- - const [selectedPosition, setSelectedPosition] = useState('전체'); // (7주차 추가: 필터 상태) - const [selectedStatus, setSelectedStatus] = useState('모집 중'); // (7주차 추가: 상태 필터) + 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 ( -
-
- - {/* [1] 필터 구역 (컴포넌트화 전, 상태 연동 준비) */} -
-
- {/* 1-1. 구인 포지션 필터 */} -
-
구인 포지션
-
- {/* [자바스크립트 문법] .map()을 사용한 리스트 렌더링 */} - {positions.map((pos) => ( - - ))} +
+ {/* [메인 영역] pt-64: 모바일에서 길어진 헤더 높이만큼 상단 여백(Padding-Top)을 충분히 확보 */} +
+ + {/* [1] 필터 구역 */} +
+
+ + {/* 1-1. 구인 포지션 필터 (모바일에서는 세로 레이아웃) */} +
+
+ 구인 포지션 +
+
+ {/* [자바스크립트 문법] .map()을 사용하여 배열 데이터만큼 버튼 생성 */} + {positions.map((pos) => ( + + ))} +
-
- {/* 1-2. 모집 여부 필터 */} -
-
모집 여부
-
- {/* [자바스크립트 문법] 즉석 배열(['모집 중', '완료'])을 생성하여 map 실행 */} - {['모집 중', '완료'].map((st) => ( - - ))} + {/* 1-2. 모집 여부 필터 */} +
+
+ 모집 여부 +
+
+ {/* [자바스크립트 문법] 즉석에서 생성한 배열(['모집 중', '완료'])로 map 실행 */} + {['모집 중', '완료'].map((st) => ( + + ))} +
-
- {/* [2] 리스트 구역 (컴포넌트 적용) */} -
-
-
모집 여부
-
구인 포지션
-
프로젝트 모집 글
-
작성자
-
업로드 시간
-
+ {/* [2] 리스트 구역 */} +
+ {/* 리스트 헤더: sm(640px) 이상 화면에서만 보이도록 설정 */} +
+
여부
+
포지션
+
프로젝트 모집 글
+
작성자
+
업로드 시간
+
- {/* (7주차 수정: 개별 행을 ProjectRow 컴포넌트로 대체) */} - {/* [자바스크립트/React] 사용자 정의 컴포넌트에 데이터를 전달(Props)하는 파트 */} - - - - - - -
+ {/* [자바스크립트/React] 사용자 정의 컴포넌트 ProjectRow에 데이터를 전달(Props) */} + + + + + + +
- {/* [3] 페이지네이션 */} -
- - {totalPages.map((num) => ( - - ))} - -
-
-
+ {totalPages.map((num) => ( + + ))} + +
+
+
); } \ No newline at end of file From 8dd10ba61cd49790c62f404cde58d7878e0c5265 Mon Sep 17 00:00:00 2001 From: Asterisk-_ Date: Tue, 6 Jan 2026 02:45:47 +0900 Subject: [PATCH 4/4] =?UTF-8?q?refactor:=20=EC=8A=A4=ED=84=B0=EB=94=94=20?= =?UTF-8?q?=EB=B0=8F=20=ED=94=84=EB=A1=9C=EC=A0=9D=ED=8A=B8=20=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20=ED=83=80=EC=9E=85=20=EC=95=88=EC=A0=95?= =?UTF-8?q?=EC=84=B1=20=EA=B0=95=ED=99=94=20=EB=B0=8F=20=EC=83=81=EC=88=98?= =?UTF-8?q?=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 타입 안정성 강화 및 데이터 구조 최적화, 컴포넌트 독립성 확보 --- src/app/project/page.tsx | 170 ++++++--------------- src/app/study/page.tsx | 205 +++++++++++--------------- src/components/project/ProjectRow.tsx | 90 +++++++++++ src/components/shared/Pagination.tsx | 45 ++++++ src/components/study/StudyCard.tsx | 68 +++++++++ 5 files changed, 330 insertions(+), 248 deletions(-) create mode 100644 src/components/project/ProjectRow.tsx create mode 100644 src/components/shared/Pagination.tsx create mode 100644 src/components/study/StudyCard.tsx diff --git a/src/app/project/page.tsx b/src/app/project/page.tsx index f691bfe..52a4397 100644 --- a/src/app/project/page.tsx +++ b/src/app/project/page.tsx @@ -2,110 +2,42 @@ 'use client'; import { useState } from 'react'; +// [수정] 컴포넌트와 함께 정의된 유니온 타입들을 가져옵니다. (별칭 PJR 사용) +import { ProjectRow as PJR, ProjectStatus, ProjectPosition } from '@/components/project/ProjectRow'; +import PGN from '@/components/shared/Pagination'; -// --- (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 ( -
- {/* [반응형 레이아웃 1] 모바일 카드형 (sm 미만에서 표시, sm 이상에서 hidden) */} -
- {/* 상단: 상태 및 포지션 배지 영역 */} -
- {/* [자바스크립트 문법] 템플릿 리터럴(` `)과 삼항 연산자를 사용한 동적 클래스 부여 */} - - {status} - - - {position} - -
+// -------------------------------------------------------------------------- +// 기존에 ProjectPage 함수 내부에 선언되어 있던 정적 데이터 배열들을 함수 외부(상단)로 이동 +// 리렌더링마다 배열이 재생성되는 것을 방지하기 위함 +// -------------------------------------------------------------------------- - {/* 하단: 제목(왼쪽 정렬)과 작성자/시간(오른쪽 정렬) */} -
-
- {title} - {/* [자바스크립트 문법] && (단락 평가): comments가 있을 때만 💬 아이콘 렌더링 */} - {comments && 💬 {comments}} -
- - {/* 작성자 및 업로드 시간을 세로로 배치 */} -
- {author} - {time} -
-
-
- - {/* [반응형 레이아웃 2] 태블릿/PC 그리드형 (sm 이상에서 표시, sm 미만에서 hidden) */} -
-
- - {status} - -
-
- - {position} - -
-
- {title} - {comments && 💬 {comments}} -
-
{author}
-
{time}
-
-
- ); -} +// [자바스크립트 문법] 상수 데이터 배열 정의 (수정: 외부로 이동 및 유니온 타입 적용) +const POSITIONS: ProjectPosition[] = ['전체', '프론트엔드', '백엔드', '개발', '디자인', '기획', '기타']; +const TOTAL_PAGES = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; // [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]; + + // [수정] useState 뒤에 <타입>을 명시하여 지정된 값만 상태로 가질 수 있게 함 + const [selectedPosition, setSelectedPosition] = useState('전체'); // 현재 선택된 포지션 필터 + const [selectedStatus, setSelectedStatus] = useState('모집 중'); // 현재 선택된 모집 여부 필터 // --- (새로운 기능: 클릭 이벤트 핸들러) --- // [자바스크립트 문법] 화살표 함수를 사용하여 클릭 시 상태 변경과 콘솔 기록을 동시에 수행 - const handlePositionClick = (pos: string) => { + // [수정] 매개변수 타입을 유니온 타입으로 변경하여 잘못된 값이 들어오는 일 방지 + const handlePositionClick = (pos: ProjectPosition) => { setSelectedPosition(pos); console.log(`선택된 포지션: ${pos}`); // 브라우저 개발자 도구 콘솔에 기록 }; - const handleStatusClick = (status: string) => { + const handleStatusClick = (status: ProjectStatus) => { setSelectedStatus(status); console.log(`선택된 모집 상태: ${status}`); }; + // 2번째 함수 (전체 페이지) return (
@@ -122,13 +54,13 @@ export default function ProjectPage() { 구인 포지션
- {/* [자바스크립트 문법] .map()을 사용하여 배열 데이터만큼 버튼 생성 */} - {positions.map((pos) => ( + {/* [자바스크립트 문법] .map()을 사용하여 외부 상수의 배열 데이터만큼 버튼 생성 */} + {POSITIONS.map((pos) => (
- {/* [자바스크립트 문법] 즉석에서 생성한 배열(['모집 중', '완료'])로 map 실행 */} - {['모집 중', '완료'].map((st) => ( + {/* [자바스크립트 문법] 즉석에서 생성한 배열에 유니온 타입을 적용하여 map 실행 */} + {(['모집 중', '완료'] as ProjectStatus[]).map((st) => (
- {/* [자바스크립트/React] 사용자 정의 컴포넌트 ProjectRow에 데이터를 전달(Props) */} - - - - - - + {/* [자바스크립트/React] 분리된 사용자 정의 컴포넌트 PJR(ProjectRow)에 데이터를 전달(Props) */} + {/* 각 행은 필터와 관계없이 고유한 데이터를 보여줍니다. */} + + + + + +
- {/* [3] 페이지네이션 */} -
- - {totalPages.map((num) => ( - - ))} - -
+ {/* [3] 페이지네이션 (분리된 공통 컴포넌트 PGN 사용) */} + {/* [변경사항] 기존의 복잡한 버튼 로직을 삭제하고 외부로 이동한 TOTAL_PAGES 상수를 사용하여 PGN을 호출 */} + { + setCurrentPage(num); + console.log(`페이지 이동: ${num}`); + }} + />
); diff --git a/src/app/study/page.tsx b/src/app/study/page.tsx index e49ee1c..8ee45a2 100644 --- a/src/app/study/page.tsx +++ b/src/app/study/page.tsx @@ -2,53 +2,37 @@ 'use client'; import { useState } from 'react'; +// [수정] 컴포넌트와 함께 정의된 유니온 타입들을 가져옴 (별칭 SDC 사용) +import { StudyCard as SDC, StudyCategory, StudyStatus } from '@/components/study/StudyCard'; +import PGN from '@/components/shared/Pagination'; -// 1. 카드 컴포넌트 (일반 React/JS 함수형 컴포넌트) -function StudyCard({ number }: { number: number }) { - return ( -
-
-
- 모집 중 - 🕑6시간 전 -
-
-

웹개발 스터디 모집합니다~

-
C++
-
-
-
-
-
- aBCDFEFGOL -
-
👁️ 122💬 333
-
-
- ); -} +// -------------------------------------------------------------------------- +// [리뷰어 피드백 반영: 위치 변경] +// 기존에 StudyPage 함수 내부에 선언되어 있던 정적 데이터 배열들을 함수 외부(상단)로 이동 +// 리렌더링마다 배열이 재생성되는 것을 방지하여 메모리 효율을 높이기 위함 +// -------------------------------------------------------------------------- +const CATEGORIES: StudyCategory[] = ['전체', 'C++', 'Python', 'Java', '알고리즘', '기타']; +const TOTAL_PAGES = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; // [Next.js 문법] export default 함수는 해당 파일의 대표 페이지가 됩니다. (App Router 라우팅) -export default function StudyPage() { //라우팅 경로에 맞게 함수명 변경 +export default function StudyPage() { // [자바스크립트/React 문법] 상태 관리를 위한 Hook 사용 - const [selected, setSelected] = useState('전체'); - const [status, setStatus] = useState('모집 중'); + // [수정] 유니온 타입을 지정하여 잘못된 문자열 입력을 방지 + 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) => { + // [수정] 매개변수 타입을 StudyCategory 유니온 타입으로 제한 + const handleCategoryClick = (category: StudyCategory) => { setSelected(category); console.log(`선택된 카테고리: ${category}`); // 개별적인 이벤트 로그 }; // 모집 상태 클릭 시 실행될 함수 - const handleStatusClick = (statusName: string) => { + // [수정] 매개변수 타입을 StudyStatus 유니온 타입으로 제한 + const handleStatusClick = (statusName: StudyStatus) => { setStatus(statusName); // 모집 중이면 true, 모집 완료면 false const isRecruiting = statusName === '모집 중'; // 왼쪽 값과 오른쪽 값을 비교하여 결과를 무조건 불리언 @@ -61,100 +45,81 @@ export default function StudyPage() { //라우팅 경로에 맞게 함수명 변 // --- (7주차 추가 끝 : 콘솔로그) --- return ( - // (7주차 수정) 전체 화면 너비를 차지하는 배경 레이어 추가 -> 사이드 검둥이 제거 -
-
- - {/* 1번 네모: 제목 */} -
-

스터디 모집 공고

-
+ // (7주차 수정) 전체 화면 너비를 차지하는 배경 레이어 추가 -> 사이드 검둥이 제거 +
+
+ + {/* 1번 네모: 제목 */} +
+

스터디 모집 공고

+
+ + {/* 2번 네모: 필터 구역 (7주차 수정: 반응형 레이아웃 적용) */} +
+ {/* !!!!(7주차 수정) flex-col로 세로 정렬하되, md: 768px 이상에서만 가로(row) 정렬, items-start로 왼쪽 정렬 */} - {/* 2번 네모: 필터 구역 (7주차 수정: 반응형 레이아웃 적용) */} -
- {/* !!!!(7주차 수정) flex-col로 세로 정렬하되, md: 768px 이상에서만 가로(row) 정렬, items-start로 왼쪽 정렬 */} + {/* 카테고리 버튼 구역 (7주차 수정: 줄바꿈 허용) */} +
+ {/* (7주차 추가) flex-wrap을 넣어 화면이 좁아지면 버튼이 다음 줄로 넘어가게 함 */} + {/* [자바스크립트 문법] .map()을 사용하여 외부 상수의 배열 데이터를 JSX 리스트로 변환 */} + {CATEGORIES.map((category) => ( + + ))} +
- {/* 카테고리 버튼 구역 (7주차 수정: 줄바꿈 허용) */} -
- {/* (7주차 추가) flex-wrap을 넣어 화면이 좁아지면 버튼이 다음 줄로 넘어가게 함 */} - {/* [자바스크립트 문법] .map()을 사용하여 배열 데이터를 JSX 리스트로 변환 JSX 안의 {} 사용*/} - {categories.map((category) => ( - - ))} + +
- {/* 모집 상태 버튼 구역 (7주차 수정: 모바일 환경에서는 숨기고 데스크톱에서만 표시) */} -
- {/* (7주차 수정) 'hidden'으로 기본 숨김 처리, 'md:flex'로 768px 이상에서만 나타나게 함 -> 모바일 환경 우선*/} - - + {/* 3번 네모: 카드 그리드 */} +
{/*카드의 반응형을 담당하는 핵심 코드*/} + {/* [자바스크립트/React] 분리된 사용자 정의 컴포넌트 SDC(StudyCard)를 호출 */} + {/* [수정] 각 카드마다 원하는 제목과 시간을 개별적으로 지정했습니다. */} + + + + + + + + +
-
- {/* 3번 네모: 카드 그리드 */} -
{/*카드의 반응형을 담당하는 핵심 코드*/} - {/* [자바스크립트 문법] 빈 배열을 생성하여 반복 렌더링 수행 */} - {[...Array(9)].map((_, i) => ( - - ))} -
- - {/* 4번 네모: 페이지네이션 */} -
- {/* [자바스크립트 문법] 화살표 함수와 Math 객체를 활용한 로직 처리 */} - - - {totalPages.map((num) => ( - - ))} - - -
-
-
+ {/* 4번 네모: 페이지네이션 (공통 컴포넌트 PGN 사용) */} + setCurrentPage(num)} + /> +
+
); } \ No newline at end of file diff --git a/src/components/project/ProjectRow.tsx b/src/components/project/ProjectRow.tsx new file mode 100644 index 0000000..f1dd79c --- /dev/null +++ b/src/components/project/ProjectRow.tsx @@ -0,0 +1,90 @@ +// [Next.js 문법] 'use client'는 이 컴포넌트가 브라우저에서 상호작용(useState, 클릭 등)을 한다는 것을 Next.js에 알림 +'use client'; + +// [타입스크립트] 유니온 타입 정의: 허용할 문자열 값을 정확히 지정합니다. +// 이렇게 하면 "모집 중", "완료" 외에 다른 문자열이 들어오면 에러가 발생하여 오타를 방지합니다. +export type ProjectStatus = '모집 중' | '완료'; +export type ProjectPosition = '전체' | '프론트엔드' | '백엔드' | '개발' | '디자인' | '기획' | '기타'; + +// --- (7주차 추가: 공통 컴포넌트 추출 - 프로젝트 리스트 아이템) --- +// [자바스크립트/React] 독립적인 UI 단위를 함수로 만든 '컴포넌트' 파트 +interface ProjectRowProps { + status: ProjectStatus; // [수정] string -> ProjectStatus 유니온 타입으로 변경 + position: ProjectPosition; // [수정] string -> ProjectPosition 유니온 타입으로 변경 + title: string; + author: string; + time: string; + comments?: number; +} + +export function ProjectRow({ + status, + position, + title, + author, + time, + comments, +}: ProjectRowProps) { + // [자바스크립트 문법] 변수 선언 및 조건식(비교 연산) + const isCompleted = status === '완료'; // isCompleted 안에 불리언(true/false) 값 저장 + + // // 1번째 함수 (재사용 가능한 UI 조각) + return ( +
+ {/* [반응형 레이아웃 1] 모바일 카드형 (sm 미만에서 표시, sm 이상에서 hidden) */} +
+ {/* 상단: 상태 및 포지션 배지 영역 */} +
+ {/* [자바스크립트 문법] 템플릿 리터럴(` `)과 삼항 연산자를 사용한 동적 클래스 부여 */} + + {status} + + + {position} + +
+ + {/* 하단: 제목(왼쪽 정렬)과 작성자/시간(오른쪽 정렬) */} +
+
+ {title} + {/* [자바스크립트 문법] && (단락 평가): comments가 있을 때만 💬 아이콘 렌더링 */} + {comments && 💬 {comments}} +
+ + {/* 작성자 및 업로드 시간을 세로로 배치 */} +
+ {author} + {time} +
+
+
+ + {/* [반응형 레이아웃 2] 태블릿/PC 그리드형 (sm 이상에서 표시, sm 미만에서 hidden) */} +
+
+ + {status} + +
+
+ + {position} + +
+
+ {title} + {comments && 💬 {comments}} +
+
{author}
+
{time}
+
+
+ ); +} \ No newline at end of file diff --git a/src/components/shared/Pagination.tsx b/src/components/shared/Pagination.tsx new file mode 100644 index 0000000..1210e21 --- /dev/null +++ b/src/components/shared/Pagination.tsx @@ -0,0 +1,45 @@ +// src/components/shared/Pagination.tsx +'use client'; + +interface PaginationProps { + currentPage: number; + totalPages: number[]; + onPageChange: (page: number) => void; +} + +export default function Pagination({ currentPage, totalPages, onPageChange }: PaginationProps) { + return ( +
+ {/* [자바스크립트 문법] 이전 페이지 이동 로직 */} + + + {/* 페이지 번호 목록 */} + {totalPages.map((num) => ( + + ))} + + {/* 다음 페이지 이동 로직 */} + +
+ ); +} \ No newline at end of file diff --git a/src/components/study/StudyCard.tsx b/src/components/study/StudyCard.tsx new file mode 100644 index 0000000..6791894 --- /dev/null +++ b/src/components/study/StudyCard.tsx @@ -0,0 +1,68 @@ +// src/components/study/StudyCard.tsx +'use client'; + +// [타입스크립트] 유니온 타입 정의: 허용할 카테고리와 상태 값을 명확히 제한합니다. +export type StudyCategory = '전체' | 'C++' | 'Python' | 'Java' | '알고리즘' | '기타'; +export type StudyStatus = '모집 중' | '모집 완료'; + +interface StudyCardProps { + number: number; + // 나중에 데이터를 받아올 때를 대비해 타입을 미리 정의해둡니다. + category?: StudyCategory; + status?: StudyStatus; + title?: string; // [추가] 카드마다 다른 제목을 받기 위한 속성 + time?: string; // [추가] 카드마다 다른 업로드 시간을 받기 위한 속성 +} + +// 1. 카드 컴포넌트 (일반 React/JS 함수형 컴포넌트) +// [수정] title과 time을 props로 추가하여 고정된 문구 대신 전달받은 데이터를 보여줍니다. +export function StudyCard({ + number, + category = 'C++', + status = '모집 중', + title = '웹개발 스터디 모집합니다~', // 기본값 + time = '6시간 전' // 기본값 +}: StudyCardProps) { + + // 모집 완료일 경우 배지 색상을 변경하기 위한 조건식 + const isCompleted = status === '모집 완료'; + + return ( +
+
+
+ {/* [수정] 전달받은 status 타입을 출력하고, 상태에 따라 색상이 변하도록 수정 */} + + {status} + + {/* [수정] 고정된 6시간 전 대신 props로 받은 time 출력 */} + 🕑{time} +
+
+ {/* [수정] 고정된 제목 대신 props로 받은 title 출력 */} +

{title}

+ {/* [수정] 전달받은 category 타입을 화면에 직접 출력 */} +
+ + {category} + +
+
+
+
+
+
+ aBCDFEFGOL +
+
+ 👁️ 122 + 💬 333 +
+
+
+ ); +} \ No newline at end of file