1- import React from "react" ;
2- import { useNavigate } from "react-router-dom" ;
1+ import React , { useEffect , useState } from "react" ;
2+ import { useNavigate } from "react-router-dom" ;
33import "./Community.css" ;
44
5+ const API_BASE = "http://52.79.145.160:8080" ;
6+
57export default function Community ( ) {
68 const navigate = useNavigate ( ) ;
79 const tabs = [ "전체" , "미해결" , "해결됨" ] ;
810 const filters = [ "최신순" , "정확도순" , "답변많은순" , "좋아요순" ] ;
911
10- const posts = [
11- {
12- status : "해결됨" ,
13- title : "버블 정렬 시각화 프로젝트 공유합니다" ,
14- summary : "버블 정렬 알고리즘을 시각적으로 이해하기 쉽게 구현해보았습니다. 피드백 부탁드려요!" ,
15- tags : [ "알고리즘" , "정렬" , "시각화" ] ,
16- author : "김코딩" ,
17- date : "2023. 5. 15" ,
18- likes : 24 ,
19- comments : 8
20- } ,
21- {
22- status : "해결됨" ,
23- title : "그래프 탐색 알고리즘 비교: BFS vs DFS" ,
24- summary : "그래프 탐색 알고리즘의 차이점을 비교하고 어떤 상황에서 더 적합한지 정리했습니다." ,
25- tags : [ "그래프" , "BFS" , "DFS" ] ,
26- author : "이알고" ,
27- date : "2023. 5. 17" ,
28- likes : 32 ,
29- comments : 12
30- } ,
31- {
32- status : "해결됨" ,
33- title : "동적 프로그래밍 문제 해결 가이드" ,
34- summary : "DP 문제를 효율적으로 푸는 전략과 예제를 모아 정리해봤습니다." ,
35- tags : [ "DP" , "문제풀이" , "코딩테스트" ] ,
36- author : "박코딩" ,
37- date : "2023. 5. 18" ,
38- likes : 40 ,
39- comments : 15
40- } ,
41- {
42- status : "해결됨" ,
43- title : "수강 중 문의드립니다!" ,
44- summary : "안녕하세요! 수업 강의 잘 듣고 있습니다! 궁금한 게 있어서 문의 남깁니다! numeric_only=True는 이번에 시험환경이 엄..." ,
45- tags : [ "python" , "머신러닝" , "빅데이터" , "pandas" , "빅데이터분석기사" ] ,
46- author : "lettig0555" ,
47- date : "2시간 전" ,
48- likes : 6 ,
49- comments : 13
50- } ,
51- {
52- status : "해결됨" ,
53- title : "rmse 값 구하기" ,
54- summary : "랜덤포레스트 후 rmse 값을 구할 때 이렇게 구해도 상관없을까요?? from sklearn.ensemble import RandomForest..." ,
55- tags : [ "python" , "머신러닝" , "빅데이터" , "pandas" , "빅데이터분석기사" ] ,
56- author : "민우" ,
57- date : "6분 전" ,
58- likes : 0 ,
59- comments : 2
60- } ,
61- {
62- status : "미해결" ,
63- title : "2025년 1회 구조체와 연결리스트 문제누락" ,
64- summary : "5페이지 구조체와 연결리스트 해설 누락된 것 같습니다." ,
65- tags : [ "python" , "java" , "c" , "정보처리기사" ] ,
66- author : "hyungjun jo" ,
67- date : "7시간 전" ,
68- likes : 0 ,
69- comments : 3
70- } ,
71- {
72- status : "해결됨" ,
73- title : "작업형2, 작업형3 pd.get_dummies 시 drop_first 유무" ,
74- summary : "작업형2 할때는 pd.get_dummies(df) 할때 drop_first가 들어가 있지 않았는데 작업형3 강의에서는 다중공선성 피해..." ,
75- tags : [ "python" , "머신러닝" , "빅데이터" , "pandas" , "빅데이터분석기사" ] ,
76- author : "섭식" ,
77- date : "9시간 전" ,
78- likes : 6 ,
79- comments : 3
80- } ,
81- {
82- status : "미해결" ,
83- title : "궁금한게 있습니다!" ,
84- summary : "학습 관련 질문을 남겨주세요. 상세히 작성하면 더 좋아요! 질문글과 관련된 영상 위치를 알려주면 더 빠르게 답변드릴 수 있어요!" ,
85- tags : [ "python" , "머신러닝" , "빅데이터" , "pandas" , "빅데이터분석기사" ] ,
86- author : "eovnffppa" ,
87- date : "9시간 전" ,
88- likes : 0 ,
89- comments : 15
90- } ,
91- {
92- status : "해결됨" ,
93- title : "안녕하세요 주말코딩님 질문드립니다" ,
94- summary : "안녕하세요 주말코딩님. 저는 완전 문과 노베이스고 지금은 대기업 하청 전산실 OP로 일하면서 정보처리기사 실기 시험을..." ,
95- tags : [ "python" , "정보처리기사" ] ,
96- author : "김재호" ,
97- date : "10시간 전" ,
98- likes : 0 ,
99- comments : 10
100- }
101- ] ;
12+ // ✅ 서버 데이터 상태
13+ const [ posts , setPosts ] = useState ( [ ] ) ;
14+ const [ loading , setLoading ] = useState ( true ) ;
15+ const [ error , setError ] = useState ( "" ) ;
16+
17+ useEffect ( ( ) => {
18+ let ignore = false ;
19+ const controller = new AbortController ( ) ;
20+
21+ ( async ( ) => {
22+ try {
23+ setLoading ( true ) ;
24+ setError ( "" ) ;
25+
26+ const token = localStorage . getItem ( "token" ) ;
27+ if ( ! token ) {
28+ // 🚨 비회원 접근 차단
29+ alert ( "로그인이 필요합니다." ) ;
30+ navigate ( "/" ) ;
31+ return ;
32+ }
33+
34+ const res = await fetch ( `${ API_BASE } /api/posts` , {
35+ method : "GET" ,
36+ headers : {
37+ Accept : "application/json" ,
38+ Authorization : `Bearer ${ token } ` ,
39+ } ,
40+ signal : controller . signal ,
41+ credentials : "include" ,
42+ } ) ;
43+
44+ if ( ! res . ok ) {
45+ const text = await res . text ( ) ;
46+ throw new Error ( text || `목록 조회 실패 (${ res . status } )` ) ;
47+ }
48+
49+ const data = await res . json ( ) ; // ← 배열 형태 가정
50+ if ( ignore ) return ;
51+
52+ // ✅ createdAt(또는 id) 기준으로 최신 글이 먼저 오도록 정렬
53+ const getTime = ( x ) => ( x ? Date . parse ( x ) || 0 : 0 ) ;
54+ const sorted = ( Array . isArray ( data ) ? data : [ ] )
55+ . slice ( )
56+ . sort ( ( a , b ) => {
57+ const diff = getTime ( b . createdAt ) - getTime ( a . createdAt ) ;
58+ if ( diff !== 0 ) return diff ;
59+ // createdAt이 같거나 비어있으면 id 내림차순으로 보정
60+ return ( b . id ?? 0 ) - ( a . id ?? 0 ) ;
61+ } ) ;
62+
63+ // 🔄 정렬된 목록을 UI 필드로 매핑
64+ const mapped = sorted . map ( ( p ) => ( {
65+ id : p . id ,
66+ status : p . status || "" ,
67+ title : p . title ,
68+ summary : ( p . content || "" ) . replace ( / < [ ^ > ] + > / g, "" ) . slice ( 0 , 120 ) ,
69+ tags : p . tags || [ ] ,
70+ author : p . writer || "익명" ,
71+ date : p . createdAt ? new Date ( p . createdAt ) . toLocaleString ( ) : "" ,
72+ likes : p . likeCount ?? 0 ,
73+ comments : p . commentCount ?? 0 ,
74+ } ) ) ;
75+
76+ setPosts ( mapped ) ;
77+ } catch ( e ) {
78+ if ( ! ignore ) setError ( e . message || "알 수 없는 오류" ) ;
79+ } finally {
80+ if ( ! ignore ) setLoading ( false ) ;
81+ }
82+ } ) ( ) ;
83+
84+ return ( ) => {
85+ ignore = true ;
86+ controller . abort ( ) ;
87+ } ;
88+ } , [ ] ) ;
10289
10390 return (
10491 < div className = "community-wrapper" >
@@ -155,42 +142,55 @@ export default function Community() {
155142 </ button >
156143 </ div >
157144
158- < div className = "post-list" >
159- { posts . map ( ( post , i ) => (
160- < div
161- key = { i }
162- className = "post-card"
163- onClick = { ( ) => navigate ( `/community/post/${ i } ` ) }
164- style = { { cursor : "pointer" } }
165- >
166- < div className = "post-meta" >
167- < div className = "title-row" >
168- < span className = { `badge ${ post . status === "해결됨" ? "badge-solved" : "" } ` } >
169- { post . status }
170- </ span >
171- < h3 className = "post-title" > { post . title } </ h3 >
145+ { /* ✅ 로딩/에러/빈 상태 */ }
146+ { loading && < div className = "post-list" > < p > 불러오는 중…</ p > </ div > }
147+ { ! loading && error && < div className = "post-list" > < p className = "error" > { error } </ p > </ div > }
148+ { ! loading && ! error && posts . length === 0 && (
149+ < div className = "post-list" > < p > 게시글이 없습니다.</ p > </ div >
150+ ) }
151+
152+ { ! loading && ! error && posts . length > 0 && (
153+ < div className = "post-list" >
154+ { posts . map ( ( post ) => (
155+ < div
156+ key = { post . id }
157+ className = "post-card"
158+ onClick = { ( ) => navigate ( `/community/post/${ post . id } ` ) } // ← id 사용
159+ style = { { cursor : "pointer" } }
160+ >
161+ < div className = "post-meta" >
162+ < div className = "title-row" >
163+ { /* 상태값 없으면 뱃지 숨김 */ }
164+ { post . status ? (
165+ < span className = { `badge ${ post . status === "해결됨" ? "badge-solved" : "" } ` } >
166+ { post . status }
167+ </ span >
168+ ) : null }
169+ < h3 className = "post-title" > { post . title } </ h3 >
170+ </ div >
171+ < p className = "post-summary" > { post . summary } </ p >
172172 </ div >
173- < p className = "post-summary" > { post . summary } </ p >
174- </ div >
175- < div className = "post-tags" >
176- { post . tags . map ( ( tag , j ) => (
177- < span key = { j } className = "tag" > { tag } </ span >
178- ) ) }
179- </ div >
180- < div className = "post-footer" >
181- < div className = "post-footer-left" >
182- < span > { post . author } </ span >
183- < span > { post . date } </ span >
173+ < div className = "post-tags" >
174+ { ( post . tags || [ ] ) . map ( ( tag , j ) => (
175+ < span key = { j } className = "tag" > { tag } </ span >
176+ ) ) }
184177 </ div >
185- < div className = "post-footer-right" >
186- < span > 👍 { post . likes } </ span >
187- < span > 💬 { post . comments } </ span >
178+ < div className = "post-footer" >
179+ < div className = "post-footer-left" >
180+ < span > { post . author } </ span >
181+ < span > { post . date } </ span >
182+ </ div >
183+ < div className = "post-footer-right" >
184+ < span > 👍 { post . likes } </ span >
185+ < span > 💬 { post . comments } </ span >
186+ </ div >
188187 </ div >
189188 </ div >
190- </ div >
191- ) ) }
192- </ div >
189+ ) ) }
190+ </ div >
191+ ) }
193192
193+ { /* 기존 페이징 UI는 유지 (서버 페이징 스펙 나오면 연결) */ }
194194 < div className = "pagination-wrapper" >
195195 < div className = "page-numbers" >
196196 < button className = "page-button active" > 1</ button >
@@ -215,26 +215,11 @@ export default function Community() {
215215 < div className = "popular-posts" >
216216 < h4 > 주간 인기글</ h4 >
217217 < ul >
218- < li >
219- < div className = "post-title" > 버블 정렬 시각화 프로젝트 공유합니다</ div >
220- < div className = "post-author" > 김코딩</ div >
221- </ li >
222- < li >
223- < div className = "post-title" > 그래프 탐색 알고리즘 비교: BFS vs DFS</ div >
224- < div className = "post-author" > 이알고</ div >
225- </ li >
226- < li >
227- < div className = "post-title" > 동적 프로그래밍 문제 해결 가이드</ div >
228- < div className = "post-author" > 박코딩</ div >
229- </ li >
230- < li >
231- < div className = "post-title" > 백엔드 신입 CS 스터디 3기 모집</ div >
232- < div className = "post-author" > 김지훈</ div >
233- </ li >
234- < li >
235- < div className = "post-title" > AI 실전 활용을 위한 4주 집중 스터디, 애사모!</ div >
236- < div className = "post-author" > Edun</ div >
237- </ li >
218+ < li > < div className = "post-title" > 버블 정렬 시각화 프로젝트 공유합니다</ div > < div className = "post-author" > 김코딩</ div > </ li >
219+ < li > < div className = "post-title" > 그래프 탐색 알고리즘 비교: BFS vs DFS</ div > < div className = "post-author" > 이알고</ div > </ li >
220+ < li > < div className = "post-title" > 동적 프로그래밍 문제 해결 가이드</ div > < div className = "post-author" > 박코딩</ div > </ li >
221+ < li > < div className = "post-title" > 백엔드 신입 CS 스터디 3기 모집</ div > < div className = "post-author" > 김지훈</ div > </ li >
222+ < li > < div className = "post-title" > AI 실전 활용을 위한 4주 집중 스터디, 애사모!</ div > < div className = "post-author" > Edun</ div > </ li >
238223 </ ul >
239224 </ div >
240225 </ aside >
0 commit comments