Skip to content
Merged
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
49 changes: 49 additions & 0 deletions src/app/api/projects/[id]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/**
* GET /api/projects/:id — 상세 정보 (년도, 구분, 유형, 연구기간, 특성화트랙, 지도교수, 연구실, 참여기업, 참여학생)
*/
import { prisma } from '@/lib/db';
import { requireRole } from '@/lib/auth';
import { ok, fail, handle } from '@/lib/http';
import { maskName } from '@/lib/list-filters';

type Ctx = { params: { id: string } };

export async function GET(_req: Request, { params }: Ctx) {
return handle(async () => {
await requireRole('ADMIN');
const it = await prisma.project.findUnique({
where: { id: params.id },
include: {
company: { select: { id: true, name: true } },
lab: { select: { professorName: true, labName: true } },
students: { include: { student: { select: { studentNo: true, nameMasked: true } } } },
},
});
if (!it) return fail('프로젝트를 찾을 수 없습니다.', 404);

const named = it.students
.filter((s) => !!s.student.nameMasked)
.map((s) => ({ studentNo: s.student.studentNo, nameMasked: s.student.nameMasked as string }));
const raws = it.studentNamesRaw
? it.studentNamesRaw.split(',').filter(Boolean).map((n) => ({ studentNo: null, nameMasked: maskName(n) }))
: [];

const row = {
id: it.id,
year: it.year,
dept: it.dept,
category: it.category,
type: it.type,
title: it.title,
period: it.period,
track: it.track,
professorName: it.lab?.professorName ?? null,
labName: it.lab?.labName ?? null,
companyId: it.company?.id ?? null,
companyName: it.company?.name ?? it.companyNameRaw ?? '-',
students: [...named, ...raws],
};

return ok(row);
});
}
69 changes: 67 additions & 2 deletions src/app/students/[studentNo]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,27 @@
'use client';

import { useState } from 'react';
import { useRouter } from 'next/navigation';
import useSWR from 'swr';
import PageHeader from '@/components/PageHeader';
import type { StudentDetail, ProgramMap } from '@/lib/student-shape';

type ProjectDetail = {
id: string;
year: number | null;
dept: string | null;
category: string | null;
type: string | null;
title: string | null;
period: string | null;
track: string | null;
professorName: string | null;
labName: string | null;
companyId: string | null;
companyName: string;
students: { studentNo: string | null; nameMasked: string }[];
};

function ProgramGrid({ title, data }: { title: string; data: ProgramMap }) {
const entries = ['program1', 'program2', 'program3', 'program4', 'program5'] as (keyof ProgramMap)[];
return (
Expand All @@ -24,7 +41,11 @@ function ProgramGrid({ title, data }: { title: string; data: ProgramMap }) {

export default function StudentDetailPage({ params }: { params: { studentNo: string } }) {
const router = useRouter();
const [selectedProjectId, setSelectedProjectId] = useState<string | null>(null);
const { data: s, isLoading } = useSWR<StudentDetail>(`/api/students/${params.studentNo}`);
const { data: projectDetail } = useSWR<ProjectDetail>(
selectedProjectId ? `/api/projects/${selectedProjectId}` : null
);

if (isLoading && !s) return <div className="loading">불러오는 중…</div>;
if (!s) return <div className="empty">학생을 찾을 수 없습니다.</div>;
Expand Down Expand Up @@ -94,12 +115,12 @@ export default function StudentDetailPage({ params }: { params: { studentNo: str
<thead><tr><th className="center" style={{ width: 56 }}>연도</th><th>과제명</th><th>기간</th><th>지도교수</th><th>기업</th></tr></thead>
<tbody>
{s.projects.map((p) => (
<tr key={p.id}>
<tr key={p.id} className="row-click" onClick={() => setSelectedProjectId(p.id)}>
<td className="center">{p.year ?? '-'}</td>
<td>{p.title || '-'}</td>
<td>{p.period || '-'}</td>
<td>{p.professorName || '-'}</td>
<td>{p.companyId ? <span className="link" onClick={() => router.push(`/companies/${p.companyId}`)}>{p.companyName}</span> : <span className="muted">{p.companyName}</span>}</td>
<td>{p.companyId ? <span className="link" onClick={(e) => { e.stopPropagation(); router.push(`/companies/${p.companyId}`); }}>{p.companyName}</span> : <span className="muted">{p.companyName}</span>}</td>
</tr>
))}
</tbody>
Expand Down Expand Up @@ -128,6 +149,50 @@ export default function StudentDetailPage({ params }: { params: { studentNo: str
</div>
)}
</div>

{selectedProjectId && (
<div className="modal-root">
<div className="modal-backdrop" onClick={() => setSelectedProjectId(null)} />
<div className="modal-card">
{projectDetail ? (
<>
<h3 className="modal-title">{projectDetail.title || '프로젝트 상세'}</h3>
<div className="info-list">
<div className="info-row"><span className="info-label">연도</span><span className="info-value">{projectDetail.year ?? '-'}</span></div>
<div className="info-row"><span className="info-label">구분</span><span className="info-value">{projectDetail.category || '-'}{projectDetail.dept ? ` · ${projectDetail.dept}` : ''}</span></div>
<div className="info-row"><span className="info-label">유형</span><span className="info-value">{projectDetail.type || '-'}</span></div>
<div className="info-row"><span className="info-label">연구기간</span><span className="info-value">{projectDetail.period || '-'}</span></div>
<div className="info-row"><span className="info-label">특성화트랙</span><span className="info-value">{projectDetail.track || '-'}</span></div>
<div className="info-row"><span className="info-label">지도교수</span><span className="info-value">{projectDetail.professorName || '-'}</span></div>
<div className="info-row"><span className="info-label">연구실</span><span className="info-value">{projectDetail.labName || '-'}</span></div>
<div className="info-row"><span className="info-label">참여기업</span><span className="info-value">
{projectDetail.companyId
? <span className="link" style={{ cursor: 'pointer' }} onClick={() => router.push(`/companies/${projectDetail.companyId}`)}>{projectDetail.companyName}</span>
: projectDetail.companyName}
</span></div>
</div>
<div style={{ marginTop: 16 }}>
<div className="info-label" style={{ marginBottom: 6 }}>참여학생 ({projectDetail.students.length}명)</div>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 8 }}>
{projectDetail.students.length
? projectDetail.students.map((st, i) => (
st.studentNo
? <span key={i} className="tag tag-indigo" style={{ cursor: 'pointer' }} onClick={() => { setSelectedProjectId(null); router.push(`/students/${st.studentNo}`); }}>{st.nameMasked}</span>
: <span key={i} className="tag tag-indigo">{st.nameMasked}</span>
))
: <span className="muted">기록 없음</span>}
</div>
</div>
<div className="form-actions">
<button className="btn btn-primary" onClick={() => setSelectedProjectId(null)}>닫기</button>
</div>
</>
) : (
<div className="loading" style={{ padding: '40px 0' }}>불러오는 중…</div>
)}
</div>
</div>
)}
</>
);
}
Loading