모든 응답은 공통 응답 래퍼인 ApiResponse 형식으로 반환한다. 값이 null인 필드는 직렬화할 때 제외한다.
문항은 position 오름차순으로 정렬하고, position이 같으면 id 오름차순으로 반환한다. 프론트엔드는 먼저 common 문항을 조회해 화면에 표시하고, 지원자 유형으로 yb 또는 ob를 선택하면 해당 유형의 문항을 추가로 조회한다.
id는 문항의 식별자이며, 화면에 표시되는 순서는 position으로 결정한다. 두 값은 같은 순번으로 맞춘다(common 19, 18, yb 10ob 19~23). 체크리스트 문항은 각 유형의 맨 앞에 배치한다.
지원자 유형(common, yb, ob)에 해당하는 지원서 문항 목록을 조회한다.
GET /api/questions/{applicant_type}
| Name | Type | Required | Description | Example |
|---|---|---|---|---|
| applicant_type | string | Y | 지원자 유형 (common, yb, ob) | yb |
(없음)
(없음)
(없음)
(없음)
| Name | Type | Required | Description | Example |
|---|---|---|---|---|
| success | boolean | Y | 요청 성공 여부 | true |
| status | integer | Y | HTTP 상태 코드 | 200 |
| message | string | Y | 응답 메시지 | 요청에 성공했습니다. |
| data | object | Y | 응답 데이터 | - |
| data.applicant_type | string | Y | 조회한 지원자 유형 | yb |
| data.questions | array | Y | 문항 목록 | - |
| data.questions[].id | integer | Y | 문항 ID | 14 |
| data.questions[].question | string | Y | 문항 내용 | 1. KHUDA에 지원하게 된 계기는 무엇인가요? |
| data.questions[].applicant_type | string | Y | 문항 대상 유형 | yb |
| data.questions[].field_type | string | Y | 입력 필드 타입 | textarea |
| data.questions[].required | boolean | Y | 필수 여부 | true |
| data.questions[].max_len | integer | N | 최대 입력 길이 (없으면 생략) | 500 |
| data.questions[].options | array | N | 선택지 목록 (select, checklist용, 없으면 생략) | ["남", "여"] |
| data.questions[].position | integer | Y | 정렬 순서 | 14 |
| code | string | N | 에러 코드 (실패 시) | (없음) |
| meta | object | N | 메타 정보 (실패 시) | (없음) |
지원하기 페이지 진입 시 가장 먼저 조회되는 공통 인적사항 문항이다.
{
"success": true,
"status": 200,
"message": "요청에 성공했습니다.",
"data": {
"applicant_type": "common",
"questions": [
{
"id": 1,
"question": "이름",
"applicant_type": "common",
"field_type": "text",
"required": true,
"max_len": 20,
"position": 1
},
{
"id": 2,
"question": "성별",
"applicant_type": "common",
"field_type": "select",
"required": true,
"options": ["남", "여"],
"position": 2
},
{
"id": 3,
"question": "생년월일",
"applicant_type": "common",
"field_type": "date",
"required": true,
"position": 3
},
{
"id": 4,
"question": "연락처",
"applicant_type": "common",
"field_type": "text",
"required": true,
"max_len": 20,
"position": 4
},
{
"id": 5,
"question": "이메일",
"applicant_type": "common",
"field_type": "text",
"required": true,
"max_len": 60,
"position": 5
},
{
"id": 6,
"question": "재학 상태",
"applicant_type": "common",
"field_type": "select",
"required": true,
"options": ["재학", "휴학", "졸업유예"],
"position": 6
},
{
"id": 7,
"question": "학과",
"applicant_type": "common",
"field_type": "department",
"required": true,
"position": 7
},
{
"id": 8,
"question": "학년",
"applicant_type": "common",
"field_type": "select",
"required": true,
"options": ["1학년", "2학년", "3학년", "4학년", "5학년 이상"],
"position": 8
},
{
"id": 9,
"question": "개인정보 수집 및 이용 동의",
"applicant_type": "common",
"field_type": "consent",
"required": true,
"position": 9
}
]
}
}체크리스트(id 10)가 유형 문항 중 가장 앞에 온다.
{
"success": true,
"status": 200,
"message": "요청에 성공했습니다.",
"data": {
"applicant_type": "yb",
"questions": [
{
"id": 10,
"question": "지원서 작성 전 아래 내용을 모두 확인해주세요.",
"applicant_type": "yb",
"field_type": "checklist",
"required": true,
"options": [
"YB는 수료 조건을 반드시 이행하여야 함을 확인했습니다.",
"지원하기 탭에서 수료 조건 내용을 확인했습니다.",
"OT는 7월 13일에 진행되며, 필수로 참여해야 함을 확인했습니다."
],
"position": 10
},
{
"id": 11,
"question": "파이썬(Python) 활용 단계를 선택해주세요.",
"applicant_type": "yb",
"field_type": "python",
"required": true,
"position": 11
},
{
"id": 12,
"question": "데이터·AI 분야와 관련해 들었던 이론 강의나 수업을 한 개 이상 작성해주세요.",
"applicant_type": "yb",
"field_type": "course_experience",
"required": true,
"position": 12
},
{
"id": 13,
"question": "관심 있는 심화 트랙을 선택해주세요.",
"applicant_type": "yb",
"field_type": "select",
"required": true,
"position": 13
},
{
"id": 14,
"question": "1. KHUDA에 지원하게 된 계기와 학회 활동을 통해 이루고 싶은 목표는 무엇인가요?",
"applicant_type": "yb",
"field_type": "textarea",
"required": true,
"max_len": 500,
"position": 14
},
{
"id": 15,
"question": "2. 본인이 세운 목표를 달성하기 위해 어려운 상황 속에서도 끝까지 포기하지 않았던 경험에 대해 서술해주세요.",
"applicant_type": "yb",
"field_type": "textarea",
"required": true,
"max_len": 500,
"position": 15
},
{
"id": 16,
"question": "3. 데이터·AI 관련 프로젝트나 관련 지식을 탐구해본 경험이 있다면, 그 과정과 배운 점을 함께 서술해주세요.",
"applicant_type": "yb",
"field_type": "textarea",
"required": true,
"max_len": 500,
"position": 16
},
{
"id": 17,
"question": "면접 가능 일정",
"applicant_type": "yb",
"field_type": "interview_date",
"required": true,
"position": 17
},
{
"id": 18,
"question": "면접 가능 시간",
"applicant_type": "yb",
"field_type": "interview_time",
"required": true,
"position": 18
}
]
}
}체크리스트(id 19)가 유형 문항 중 가장 앞에 온다. 스터디 세부(id 22)는 선택 입력이라 required가 false다.
{
"success": true,
"status": 200,
"message": "요청에 성공했습니다.",
"data": {
"applicant_type": "ob",
"questions": [
{
"id": 19,
"question": "지원서 작성 전 아래 내용을 모두 확인해주세요.",
"applicant_type": "ob",
"field_type": "checklist",
"required": true,
"options": [
"OB는 별도의 수료 조건이 없음을 인지했습니다.",
"지원하기 탭에서 혜택에 대한 내용을 확인했습니다.",
"OT는 7월 13일에 진행되며, 필수로 참여해야 함을 확인했습니다."
],
"position": 19
},
{
"id": 20,
"question": "참여를 희망하는 심화 트랙을 선택해주세요.",
"applicant_type": "ob",
"field_type": "select",
"required": true,
"position": 20
},
{
"id": 21,
"question": "스터디 개설 의향이 있으신가요?",
"applicant_type": "ob",
"field_type": "select",
"required": true,
"position": 21
},
{
"id": 22,
"question": "개설하고 싶은 스터디 또는 소모임이 있다면 작성해주세요.",
"applicant_type": "ob",
"field_type": "text",
"required": false,
"max_len": 120,
"position": 22
},
{
"id": 23,
"question": "1. KHUDA에 지원하게 된 계기와 학회 활동을 통해 이루고 싶은 목표는 무엇인가요?",
"applicant_type": "ob",
"field_type": "textarea",
"required": true,
"max_len": 500,
"position": 23
}
]
}
}조건부 필수: 스터디 세부(id 22)는 기본값이 required false다. 다만 스터디 개설 의향(id 21) 답변이 yes이면 필수가 된다. 프론트가 제출을 막고 백엔드도 같은 규칙으로 검증한다. id 21 답변 값은 yes 또는 no다.
{
"success": false,
"status": 404,
"message": "유효하지 않은 지원자 유형입니다.",
"code": "QUE_404_001",
"meta": {
"path": "/api/questions/xx",
"timestamp": 1719400000000
}
}{
"success": false,
"status": 500,
"message": "서버 내부 오류가 발생했습니다.",
"code": "COM_500_001",
"meta": {
"path": "/api/questions/yb",
"timestamp": 1719400000000
}
}지원자 유형과 문항별 답변을 받아 지원서를 접수한다.
POST /api/applications
(없음)
(없음)
| Name | Type | Required | Description | Example |
|---|---|---|---|---|
| Content-Type | string | Y | 요청 본문 타입 | application/json |
| Name | Type | Required | Description | Example |
|---|---|---|---|---|
| applicant_type | string | Y | 지원자 유형 (yb, ob) | yb |
| answers | object | Y | 문항 ID를 key로 하는 답변 맵 | {"1": "홍길동"} |
답변 값은 모두 문자열이다. 배열이나 객체형 답변(체크리스트, 면접 일정과 시간)은 JSON 문자열로 직렬화해 담는다.
이메일을 기준으로 1인 1제출만 허용한다. 같은 이메일이 이미 접수돼 있으면 409로 거절한다. yb, ob 구분 없이 한 사람당 한 번만 받고 대소문자는 무시한다.
{
"applicant_type": "yb",
"answers": {
"1": "홍길동",
"5": "hong@khu.ac.kr",
"9": "agree",
"10": "[\"YB는 수료 조건을 반드시 이행하여야 함을 확인했습니다.\"]",
"14": "데이터 분석에 관심이 생겨 지원했습니다.",
"17": "[\"7월 8일 (수)\"]",
"18": "{\"7월 8일 (수)\": [\"10:00\", \"10:20\"]}"
}
}| Name | Type | Required | Description | Example |
|---|---|---|---|---|
| success | boolean | Y | 요청 성공 여부 | true |
| status | integer | Y | HTTP 상태 코드 | 201 |
| message | string | Y | 응답 메시지 | 정상적으로 생성되었습니다. |
| data | object | Y | 응답 데이터 | - |
| data.application_id | integer | Y | 생성된 지원서 ID | 1001 |
| data.status | string | Y | 지원서 처리 상태 | submitted |
| code | string | N | 에러 코드 (실패 시) | (없음) |
| meta | object | N | 메타 정보 (실패 시) | (없음) |
{
"success": true,
"status": 201,
"message": "정상적으로 생성되었습니다.",
"data": {
"application_id": 1001,
"status": "submitted"
}
}{
"success": false,
"status": 400,
"message": "필수 문항이 비어 있습니다. - question_id=14",
"code": "QUE_400_001",
"meta": {
"path": "/api/applications",
"timestamp": 1719400000000
}
}{
"success": false,
"status": 400,
"message": "문항 최대 길이를 초과했습니다. - question_id=14",
"code": "QUE_400_002",
"meta": {
"path": "/api/applications",
"timestamp": 1719400000000
}
}{
"success": false,
"status": 400,
"message": "알 수 없는 문항 ID가 포함되어 있습니다.",
"code": "APP_400_001",
"meta": {
"path": "/api/applications",
"timestamp": 1719400000000
}
}{
"success": false,
"status": 409,
"message": "이미 제출된 지원서가 있습니다.",
"code": "APP_409_001",
"meta": {
"path": "/api/applications",
"timestamp": 1719400000000
}
}{
"success": false,
"status": 500,
"message": "서버 내부 오류가 발생했습니다.",
"code": "COM_500_001",
"meta": {
"path": "/api/applications",
"timestamp": 1719400000000
}
}서버 상태를 확인한다. 인프라 헬스 체크에 사용하는 엔드포인트이므로 공통 응답 래퍼를 사용하지 않는다.
GET /health
(없음)
(없음)
(없음)
(없음)
(없음)
| Name | Type | Required | Description | Example |
|---|---|---|---|---|
| status | string | Y | 서버 상태 | ok |
{
"status": "ok"
}(없음)
어드민 페이지 로그인 API다. 프론트엔드에서 전달한 Google ID 토큰을 검증하고, admins에 등록된 운영진인지 확인한 뒤 자체 JWT를 발급한다.
POST /api/admin/auth/google
(없음)
(없음)
| Name | Type | Required | Description | Example |
|---|---|---|---|---|
| Content-Type | string | Y | 요청 본문 타입 | application/json |
| Name | Type | Required | Description | Example |
|---|---|---|---|---|
| id_token | string | Y | 구글 로그인으로 받은 ID 토큰 | eyJhbGciOi... |
{
"id_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6..."
}| Name | Type | Required | Description | Example |
|---|---|---|---|---|
| success | boolean | Y | 요청 성공 여부 | true |
| status | integer | Y | HTTP 상태 코드 | 200 |
| message | string | Y | 응답 메시지 | 요청에 성공했습니다. |
| data | object | Y | 응답 데이터 | - |
| data.access_token | string | Y | 백엔드 발급 JWT | eyJhbGciOi... |
| data.token_type | string | Y | 토큰 타입 | bearer |
| data.expires_in | integer | Y | 만료까지 남은 초 | 43200 |
| data.admin | object | Y | 로그인한 운영진 정보 | - |
| data.admin.email | string | Y | 운영진 이메일 | admin@khuda.co.kr |
| data.admin.name | string | N | 이름 (없으면 생략) | 홍길동 |
| code | string | N | 에러 코드 (실패 시) | (없음) |
| meta | object | N | 메타 정보 (실패 시) | (없음) |
{
"success": true,
"status": 200,
"message": "요청에 성공했습니다.",
"data": {
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6...",
"token_type": "bearer",
"expires_in": 43200,
"admin": { "email": "admin@khuda.co.kr" }
}
}{
"success": false,
"status": 401,
"message": "유효하지 않은 구글 토큰입니다.",
"code": "AUTH_401_003",
"meta": { "path": "/api/admin/auth/google", "timestamp": 1719400000000 }
}{
"success": false,
"status": 403,
"message": "관리자 권한이 없습니다.",
"code": "AUTH_403_001",
"meta": { "path": "/api/admin/auth/google", "timestamp": 1719400000000 }
}제출된 지원서 목록을 조회한다. 운영진 인증이 필요하다. 요약 필드(이름, 이메일, 연락처, 트랙)는 문항 답변에서 도출한다.
GET /api/admin/applications
(없음)
| Name | Type | Required | Description | Example |
|---|---|---|---|---|
| applicant_type | string | N | 유형 필터 (yb, ob) | yb |
| search | string | N | 이름, 이메일, 연락처 부분 검색 | 홍길동 |
| Name | Type | Required | Description | Example |
|---|---|---|---|---|
| Authorization | string | Y | 로그인으로 받은 JWT | Bearer eyJhbGciOi... |
(없음)
(없음)
| Name | Type | Required | Description | Example |
|---|---|---|---|---|
| success | boolean | Y | 요청 성공 여부 | true |
| status | integer | Y | HTTP 상태 코드 | 200 |
| message | string | Y | 응답 메시지 | 요청에 성공했습니다. |
| data | object | Y | 응답 데이터 | - |
| data.total | integer | Y | 결과 수 | 2 |
| data.applications | array | Y | 지원서 요약 목록 | - |
| data.applications[].application_id | integer | Y | 지원서 ID | 1001 |
| data.applications[].applicant_type | string | Y | 지원자 유형 | yb |
| data.applications[].status | string | Y | 처리 상태 | submitted |
| data.applications[].created_at | string | Y | 제출 시각 | 2026-06-29T14:00:00+09:00 |
| data.applications[].name | string | N | 이름 | 홍길동 |
| data.applications[].email | string | N | 이메일 | hong@khu.ac.kr |
| data.applications[].phone | string | N | 연락처 | 010-1234-5678 |
| data.applications[].track | string | N | 트랙 | 컴퓨터 비전 |
| data.applications[].answers | array | Y | 문항과 답변 전체 (CSV 내보내기용, 상세와 같은 형태) | - |
{
"success": true,
"status": 200,
"message": "요청에 성공했습니다.",
"data": {
"total": 1,
"applications": [
{
"application_id": 1001,
"applicant_type": "yb",
"status": "submitted",
"created_at": "2026-06-29T14:00:00+09:00",
"name": "홍길동",
"email": "hong@khu.ac.kr",
"phone": "010-1234-5678",
"track": "컴퓨터 비전",
"answers": [
{ "question_id": 1, "question": "이름", "field_type": "text", "position": 1, "value": "홍길동" }
]
}
]
}
}{
"success": false,
"status": 401,
"message": "인증 토큰이 필요합니다.",
"code": "AUTH_401_001",
"meta": { "path": "/api/admin/applications", "timestamp": 1719400000000 }
}{
"success": false,
"status": 403,
"message": "관리자 권한이 없습니다.",
"code": "AUTH_403_001",
"meta": { "path": "/api/admin/applications", "timestamp": 1719400000000 }
}지원서 한 건의 상세 정보를 조회한다. 답변에 문항 메타데이터인 문항 내용과 입력 유형을 결합한 뒤 position 순으로 반환한다. 운영진 인증이 필요하다.
GET /api/admin/applications/{application_id}
| Name | Type | Required | Description | Example |
|---|---|---|---|---|
| application_id | integer | Y | 지원서 ID | 1001 |
(없음)
| Name | Type | Required | Description | Example |
|---|---|---|---|---|
| Authorization | string | Y | 로그인으로 받은 JWT | Bearer eyJhbGciOi... |
(없음)
(없음)
| Name | Type | Required | Description | Example |
|---|---|---|---|---|
| success | boolean | Y | 요청 성공 여부 | true |
| status | integer | Y | HTTP 상태 코드 | 200 |
| message | string | Y | 응답 메시지 | 요청에 성공했습니다. |
| data | object | Y | 응답 데이터 | - |
| data.application_id | integer | Y | 지원서 ID | 1001 |
| data.applicant_type | string | Y | 지원자 유형 | yb |
| data.status | string | Y | 처리 상태 | submitted |
| data.created_at | string | Y | 제출 시각 | 2026-06-29T14:00:00+09:00 |
| data.updated_at | string | N | 마지막 수정 시각 (없으면 생략) | 2026-06-30T09:00:00+09:00 |
| data.updated_by | string | N | 수정한 운영진 이메일 (없으면 생략) | admin@khuda.co.kr |
| data.answers | array | Y | 문항과 답변 목록 (position 순) | - |
| data.answers[].question_id | integer | Y | 문항 ID | 1 |
| data.answers[].question | string | Y | 문항 내용 | 이름 |
| data.answers[].field_type | string | Y | 입력 필드 타입 | text |
| data.answers[].position | integer | Y | 정렬 순서 | 1 |
| data.answers[].value | string | N | 답변 값 (없으면 생략) | 홍길동 |
{
"success": true,
"status": 200,
"message": "요청에 성공했습니다.",
"data": {
"application_id": 1001,
"applicant_type": "yb",
"status": "submitted",
"created_at": "2026-06-29T14:00:00+09:00",
"answers": [
{ "question_id": 1, "question": "이름", "field_type": "text", "position": 1, "value": "홍길동" },
{ "question_id": 5, "question": "이메일", "field_type": "text", "position": 5, "value": "hong@khu.ac.kr" }
]
}
}{
"success": false,
"status": 404,
"message": "존재하지 않는 지원서입니다.",
"code": "APP_404_001",
"meta": { "path": "/api/admin/applications/999", "timestamp": 1719400000000 }
}운영진이 지원자의 답변을 수정한다. 전달된 답변만 기존 답변에 덮어쓴 뒤 전체 답변을 다시 검증한다. 수정 범위는 답변으로 한정하며, 상태(status)는 변경하지 않는다.
PATCH /api/admin/applications/{application_id}
| Name | Type | Required | Description | Example |
|---|---|---|---|---|
| application_id | integer | Y | 지원서 ID | 1001 |
(없음)
| Name | Type | Required | Description | Example |
|---|---|---|---|---|
| Content-Type | string | Y | 요청 본문 타입 | application/json |
| Authorization | string | Y | 로그인으로 받은 JWT | Bearer eyJhbGciOi... |
| Name | Type | Required | Description | Example |
|---|---|---|---|---|
| answers | object | Y | 수정할 문항 ID와 값의 맵 | {"4": "010-0000-0000"} |
{
"answers": { "4": "010-0000-0000" }
}지원 상세 조회(6번)와 같은 형태로 수정된 지원서를 내려준다.
{
"success": true,
"status": 200,
"message": "요청에 성공했습니다.",
"data": {
"application_id": 1001,
"applicant_type": "yb",
"status": "submitted",
"created_at": "2026-06-29T14:00:00+09:00",
"updated_at": "2026-06-30T09:00:00+09:00",
"updated_by": "admin@khuda.co.kr",
"answers": [
{ "question_id": 4, "question": "연락처", "field_type": "text", "position": 4, "value": "010-0000-0000" }
]
}
}{
"success": false,
"status": 400,
"message": "필수 문항이 비어 있습니다. - question_id=4",
"code": "QUE_400_001",
"meta": { "path": "/api/admin/applications/1001", "timestamp": 1719400000000 }
}{
"success": false,
"status": 404,
"message": "존재하지 않는 지원서입니다.",
"code": "APP_404_001",
"meta": { "path": "/api/admin/applications/999", "timestamp": 1719400000000 }
}