Skip to content

Commit cdda69f

Browse files
authored
Merge pull request #133 from Pseudo-Lab/feat/front-cert-api
Feat/front cert api
2 parents 40e797f + 0c62f5a commit cdda69f

File tree

4 files changed

+109
-100
lines changed

4 files changed

+109
-100
lines changed

โ€Žcert/frontend/.env.exampleโ€Ž

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
REACT_APP_API_BASE_URL=http://localhost:8000

โ€Žcert/frontend/src/components/common/Footer.tsxโ€Ž

Lines changed: 3 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,12 @@
11
import React from 'react';
2-
import { Box, Container, Grid, Typography, Link } from '@mui/material';
2+
import { Box, Container, Typography } from '@mui/material';
33
import { colors } from '../../styles/theme';
44

55
const Footer: React.FC = () => {
66
return (
7-
<Box component="footer" sx={{ backgroundColor: colors.dark, color: '#fff', py: 4, mt: 0.5 }}>
7+
<Box component="footer" sx={{ backgroundColor: colors.dark, color: '#fff', py: 3 }}>
88
<Container>
9-
<Grid container spacing={4} className="footer-content">
10-
<Grid item xs={12} md={4} className="footer-section">
11-
<Typography variant="h6" gutterBottom>PseudoLab</Typography>
12-
<Typography>
13-
PseudoLab์€ ์ธ๊ณต์ง€๋Šฅ๊ณผ ๋ฐ์ดํ„ฐ ์‚ฌ์ด์–ธ์Šค ๋ถ„์•ผ์—์„œ ํ•จ๊ป˜ ์„ฑ์žฅํ•˜๋Š” ์ปค๋ฎค๋‹ˆํ‹ฐ์ž…๋‹ˆ๋‹ค.
14-
</Typography>
15-
</Grid>
16-
17-
<Grid item xs={12} md={4} className="footer-section">
18-
<Typography variant="h6" gutterBottom>๋งํฌ</Typography>
19-
<Box component="ul" sx={{ listStyle: 'none', p: 0 }}>
20-
{['ํ™ˆํŽ˜์ด์ง€', '์„œ๋น„์Šค', '์ปค๋ฎค๋‹ˆํ‹ฐ', '์—ฐ๋ฝ์ฒ˜'].map((link, idx) => (
21-
<li key={idx}>
22-
<Link href="#" color="#d1d5db" underline="none" sx={{ transition: colors.transition, '&:hover': { color: '#fff' } }}>
23-
{link}
24-
</Link>
25-
</li>
26-
))}
27-
</Box>
28-
</Grid>
29-
30-
<Grid item xs={12} md={4} className="footer-section">
31-
<Typography variant="h6" gutterBottom>์—ฐ๋ฝ์ฒ˜</Typography>
32-
<Typography>์ด๋ฉ”์ผ: info@pseudolab.com</Typography>
33-
<Typography>์ „ํ™”: 02-123-4567</Typography>
34-
</Grid>
35-
</Grid>
36-
37-
<Box sx={{ textAlign: 'center', pt: 4, borderTop: '1px solid rgba(255, 255, 255, 0.1)', mt: 4 }}>
9+
<Box sx={{ textAlign: 'center', pt: 1 }}>
3810
<Typography variant="body2" color="#9ca3af">
3911
&copy; 2025 PseudoLab. All rights reserved.
4012
</Typography>
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export const API_BASE_URL =
2+
import.meta.env.VITE_API_BASE_URL || "http://localhost:8000";

โ€Žcert/frontend/src/modules/Home/index.tsxโ€Ž

Lines changed: 103 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -15,60 +15,86 @@ import {
1515
DialogContent,
1616
LinearProgress,
1717
Autocomplete,
18-
InputLabel
1918
} from '@mui/material';
2019
import { ButtonProps } from "@mui/material/Button";
2120
import { Search as SearchIcon } from '@mui/icons-material';
2221
import { styled } from '@mui/material/styles';
2322
import successImg from "../../assets/success.png";
2423
import failImg from "../../assets/fail.png";
24+
import { API_BASE_URL } from "./config";
2525

26+
type IssuePayload = {
27+
applicant_name: string;
28+
recipient_email: string;
29+
season: string;
30+
course_name: string;
31+
};
2632

27-
type StudyMeta = {
28-
periods: string[];
29-
studiesByPeriod?: Record<string, string[]>;
30-
studies?: string[];
33+
type IssueResponse = {
34+
returnCode: number; // 200, 404, 500 ๋“ฑ
3135
};
3236

33-
type IssuePayload = {
37+
type ApiStudy = {
38+
id: string;
3439
name: string;
35-
email: string;
36-
period: string;
37-
studies: string[];
40+
season: number;
41+
description: string;
42+
created_at: string;
43+
updated_at: string;
3844
};
3945

40-
type IssueResponse = {
41-
returnCode: number; // 200, 404, 500 ๋“ฑ
46+
type StudyMeta = {
47+
periods: string[];
48+
studiesByPeriod: Record<string, string[]>;
49+
studies: string[];
4250
};
4351

4452
async function fetchStudyMeta(): Promise<StudyMeta> {
45-
// const res = await fetch('/api/studies/meta');
46-
// if (!res.ok) throw new Error('Failed to load meta');
47-
// return (await res.json()) as StudyMeta;
53+
const res = await fetch(`${API_BASE_URL}/certs/all-projects`);
54+
if (!res.ok) throw new Error("Failed to load meta");
55+
56+
const data = (await res.json()) as ApiStudy[];
57+
58+
// season(=period) ๊ธฐ์ค€์œผ๋กœ ๊ทธ๋ฃนํ•‘
59+
const studiesByPeriod: Record<string, string[]> = {};
60+
const studies: string[] = [];
61+
62+
for (const item of data) {
63+
const period = String(item.season);
64+
if (!studiesByPeriod[period]) {
65+
studiesByPeriod[period] = [];
66+
}
67+
studiesByPeriod[period].push(item.name);
68+
studies.push(item.name);
69+
}
70+
71+
const periods = Object.keys(studiesByPeriod).sort();
4872

49-
await new Promise(r => setTimeout(r, 300));
5073
return {
51-
periods: ["10", "11"],
52-
studiesByPeriod: {
53-
"10": ["DevFactory", "JobPT", "3D Vision"],
54-
"11": ["AI Research Club", "Test Study"]
55-
},
56-
studies: ["DevFactory", "JobPT", "AI Research Club", "3D Vision", "Test Study"]
74+
periods,
75+
studiesByPeriod,
76+
studies,
5777
};
5878
}
5979

60-
async function issueCertificate(payload: IssuePayload): Promise<IssueResponse> {
61-
const body = JSON.stringify(payload);
62-
console.log('body', body);
63-
// const res = await fetch('/api/certificates/issue', {
64-
// method: 'POST',
65-
// headers: { 'Content-Type': 'application/json' },
66-
// body: JSON.stringify(payload),
67-
// });
68-
// if (!res.ok) throw new Error('Issue API failed');
69-
// return (await res.json()) as IssueResponse;
70-
await new Promise(r => setTimeout(r, 500));
71-
return { returnCode: 200 };
80+
async function issueCertificate(payload: IssuePayload): Promise<IssueResponse> {
81+
const res = await fetch(`${API_BASE_URL}/certs/create`, {
82+
method: "POST",
83+
headers: { "Content-Type": "application/json" },
84+
body: JSON.stringify(payload),
85+
});
86+
87+
let json: any = {};
88+
try {
89+
json = await res.json();
90+
} catch {
91+
json = {};
92+
}
93+
94+
return {
95+
...json,
96+
returnCode: res.status, // HTTP ์ƒํƒœ ์ฝ”๋“œ ๊ทธ๋Œ€๋กœ ์‚ฌ์šฉ
97+
};
7298
}
7399

74100
const SuccessIcon: React.FC = () => {
@@ -259,8 +285,6 @@ const ExportCertificateForm = () => {
259285

260286
const [returnCode, setReturnCode] = useState<number | null>(null);
261287

262-
const [testReturnCode, setTestReturnCode] = useState<number>(200);
263-
264288
useEffect(() => {
265289
let mounted = true;
266290
(async () => {
@@ -323,34 +347,60 @@ const ExportCertificateForm = () => {
323347
setIsComplete(false);
324348
setReturnCode(null);
325349
setProgress(0);
326-
350+
327351
// ๊ฐ€์งœ ์ง„ํ–‰๋ฐ”
328352
const interval = setInterval(() => {
329353
setProgress(prev => {
330354
if (prev >= 100) {
331355
clearInterval(interval);
332356
return 100;
333357
}
334-
return prev + 10;
358+
return prev + 1;
335359
});
336-
}, 150);
337-
360+
}, 200); // 0.2์ดˆ๋งˆ๋‹ค 1% โ†’ ์ด 20์ดˆ
361+
338362
try {
339-
// ์‹ค์ œ API ํ˜ธ์ถœ (์‘๋‹ต returnCode๋Š” ๋ฌด์‹œํ•˜๊ณ , UI์—์„œ ์„ ํƒํ•œ testReturnCode๋ฅผ ๋ฐ˜์˜)
340-
await issueCertificate({
341-
name: formData.name.trim(),
342-
email: formData.email.trim(),
343-
period: formData.period,
344-
studies: tags,
363+
// ๊ฐ ํƒœ๊ทธ๋ณ„ ํ˜ธ์ถœ์„ allSettled๋กœ ์ˆ˜ํ–‰ํ•˜์—ฌ ๋ถ€๋ถ„ ์‹คํŒจ๋„ ์ˆ˜์ง‘
364+
const settled = await Promise.allSettled(
365+
tags.map(tag =>
366+
issueCertificate({
367+
applicant_name: formData.name.trim(),
368+
recipient_email: formData.email.trim(),
369+
season: formData.period, // ๊ธฐ์กด ๋กœ์ง ์œ ์ง€
370+
course_name: tag, // ๊ฐ ์Šคํ„ฐ๋””๋ณ„ ํ˜ธ์ถœ
371+
})
372+
)
373+
);
374+
375+
// ํƒœ๊ทธ๋ณ„ ์ฝ”๋“œ ์ˆ˜์ง‘
376+
const perTagResults = settled.map((r, idx) => {
377+
if (r.status === "fulfilled") {
378+
return { tag: tags[idx], code: r.value.returnCode };
379+
} else {
380+
return { tag: tags[idx], code: 500 }; // ๋„คํŠธ์›Œํฌ ์˜ค๋ฅ˜ ๊ฐ™์€ ๊ฒฝ์šฐ
381+
}
345382
});
346-
347-
// ํ…Œ์ŠคํŠธ์šฉ returnCode ์ ์šฉ
348-
// 200: ์„ฑ๊ณต, 300: ์ผ๋ฐ˜ ์‹คํŒจ, ๋‚˜๋จธ์ง€(์˜ˆ: 302): โ€œ๋ช…๋‹จ์— ์—†์Œโ€ ์‹œ๋‚˜๋ฆฌ์˜ค
349-
setReturnCode(testReturnCode);
383+
384+
// ๋ชจ๋‘ 200์ด๋ฉด 200
385+
// 500์ด ํ•˜๋‚˜๋ผ๋„ ์žˆ์œผ๋ฉด 500 ("๋ช…๋‹จ์— ์—†์Œ" ์‹œ๋‚˜๋ฆฌ์˜ค)
386+
// ๊ทธ ์™ธ 300(์ผ๋ฐ˜ ์‹คํŒจ)
387+
const codes = perTagResults.map(r => r.code);
388+
let overall: number;
389+
390+
if (codes.every(c => c === 200)) {
391+
overall = 200;
392+
} else if (codes.some(c => c === 500)) {
393+
overall = 500;
394+
} else {
395+
overall = 300;
396+
}
397+
398+
setReturnCode(overall);
350399
} catch (e) {
351-
// ํ˜ธ์ถœ ์ž์ฒด ์‹คํŒจ ์‹œ ์ผ๋ฐ˜ ์‹คํŒจ๋กœ ์ฒ˜๋ฆฌ
352-
setReturnCode(300);
400+
// ์˜ˆ์™ธ์ ์œผ๋กœ ์—ฌ๊ธฐ๊นŒ์ง€ ๋–จ์–ด์ง€๋ฉด ์ผ๋ฐ˜ ์‹คํŒจ
401+
setReturnCode(500);
353402
} finally {
403+
clearInterval(interval); // ์•ˆ์ „ํ•˜๊ฒŒ ์ธํ„ฐ๋ฒŒ ์ •๋ฆฌ
354404
setIsLoading(false);
355405
setIsComplete(true);
356406
}
@@ -499,22 +549,6 @@ const ExportCertificateForm = () => {
499549
/>
500550
</FieldRow>
501551

502-
<FieldRow label="ํ…Œ์ŠคํŠธ ์ฝ”๋“œ">
503-
<StyledFormControl fullWidth size="medium">
504-
<InputLabel id="returncode-label">returnCode (ํ…Œ์ŠคํŠธ)</InputLabel>
505-
<Select
506-
labelId="returncode-label"
507-
value={String(testReturnCode)}
508-
label="returnCode (ํ…Œ์ŠคํŠธ)"
509-
onChange={(e) => setTestReturnCode(Number(e.target.value))}
510-
>
511-
<MenuItem value="200">200 (์„ฑ๊ณต)</MenuItem>
512-
<MenuItem value="404">404 (๋ช…๋‹จ ์—†์Œ)</MenuItem>
513-
<MenuItem value="500">500 (์‹คํŒจ)</MenuItem>
514-
</Select>
515-
</StyledFormControl>
516-
</FieldRow>
517-
518552
{/* Buttons */}
519553
<Box sx={{ display: 'flex', gap: 3, mt: 2 }}>
520554
<StyledButton
@@ -611,7 +645,7 @@ const ExportCertificateForm = () => {
611645
</Box>
612646
)}
613647

614-
{!isLoading && returnCode === 500 && (
648+
{!isLoading && returnCode === 300 && (
615649
/* ์ผ๋ฐ˜ ์‹คํŒจ */
616650
<Box>
617651
<FailIcon />
@@ -631,7 +665,7 @@ const ExportCertificateForm = () => {
631665
</Box>
632666
)}
633667

634-
{!isLoading && returnCode === 404 && (
668+
{!isLoading && returnCode === 500 && (
635669
/* ๋ช…๋‹จ ์—†์Œ */
636670
<Box>
637671
<FailIcon />

0 commit comments

Comments
ย (0)