- 회원가입:
/users/register
엔드포인트를 통해 회원가입합니다. - 로그인:
/auth/login
엔드포인트를 통해 로그인하고 토큰을 받습니다. - 인증 요청: 액세스 토큰을 HTTP 헤더에 포함하여 요청합니다.
Authorization: Bearer {accessToken}
- 토큰 새로 받기: 액세스 토큰이 만료되면
/auth/refresh
엔드포인트를 통해 리프레시 토큰으로 새로운 토큰을 받습니다.
- 내 그룹 조회:
/groups
엔드포인트로 현재 사용자가 속한 그룹 목록을 조회합니다. 각 그룹에 대해 자신이 관리자인지 여부(isAdmin)도 확인할 수 있습니다. - 그룹 생성: 관리자 권한으로
/groups
엔드포인트에 POST 요청을 보내 그룹을 생성합니다. - 그룹 멤버 관리:
/group-members
엔드포인트를 통해 멤버를 추가하고 관리합니다.
- 스케줄 생성: 그룹 관리자는
/schedules
엔드포인트를 통해 스케줄을 생성합니다. - 출석 체크: 사용자는
/attendances/check
엔드포인트를 통해 자신의 위치 정보를 전송하여 출석을 체크합니다.
- 버스 템플릿 생성: 관리자는
/bus-templates
엔드포인트를 통해 버스 레이아웃 템플릿을 생성합니다. - 스케줄에 버스 좌석 맵핑:
/bus-seats/batch
엔드포인트를 통해 특정 스케줄에 버스 좌석을 일괄 생성합니다. - 좌석 배정:
/bus-seats/assign
엔드포인트를 통해 특정 사용자를 특정 좌석에 배정합니다.# MeetQueue Server
NestJS와 TypeORM을 활용한 학생 출석 관리 API 서버입니다. DDD(Domain-Driven Design) 아키텍처를 기반으로 구현되었습니다.
- NestJS: 백엔드 프레임워크
- TypeORM: ORM
- MySQL: 데이터베이스
- bcrypt: 비밀번호 해싱
# 패키지 설치
$ npm install --legacy-peer-deps
# 개발 모드 실행
$ npm run start:dev
# 프로덕션 모드 실행
$ npm run start:prod
.env
파일을 프로젝트 루트에 생성하고 다음 설정을 추가합니다:
DB_HOST=localhost
DB_PORT=3306
DB_USERNAME=root
DB_PASSWORD=password
DB_DATABASE=meetqueue
DB_SYNCHRONIZE=true
CREATE TABLE `tbl_user` (
`UUID` varchar(36) NOT NULL PRIMARY KEY,
`VARCHAR(50)` varchar(255) NULL,
`VARCHAR(100)` varchar(255) NULL UNIQUE,
`VARCHAR` varchar(255) NULL
);
CREATE TABLE `tbl_group` (
`groupId` varchar(36) NOT NULL PRIMARY KEY,
`groupName` varchar(255) NULL
);
CREATE TABLE `tbl_group_member` (
`UUID` varchar(36) NOT NULL,
`groupId` varchar(36) NOT NULL,
`isAdmin` boolean DEFAULT false,
PRIMARY KEY (`UUID`, `groupId`),
FOREIGN KEY (`UUID`) REFERENCES `tbl_user` (`UUID`),
FOREIGN KEY (`groupId`) REFERENCES `tbl_group` (`groupId`)
);
CREATE TABLE `tbl_schedule` (
`scheduleId` varchar(36) NOT NULL PRIMARY KEY,
`groupId` varchar(36) NOT NULL,
`scheduleName` varchar(255) NULL,
`location` varchar(255) NULL,
`scheduleTime` datetime NULL,
`detailLocation` varchar(255) NULL,
`detailLocation2` varchar(255) NULL,
`locationRange` varchar(255) NULL,
FOREIGN KEY (`groupId`) REFERENCES `tbl_group` (`groupId`)
);
CREATE TABLE `tbl_attendance` (
`attendanceId` varchar(36) NOT NULL PRIMARY KEY,
`scheduleId` varchar(36) NOT NULL,
`userId` varchar(36) NOT NULL,
`distanceFromLocation` float NULL,
`attendanceTime` datetime NULL,
`status` varchar(20) DEFAULT '미출석',
FOREIGN KEY (`scheduleId`) REFERENCES `tbl_schedule` (`scheduleId`),
FOREIGN KEY (`userId`) REFERENCES `tbl_user` (`UUID`)
);
CREATE TABLE `tbl_bus_template` (
`id` varchar(36) NOT NULL PRIMARY KEY,
`name` varchar(100) NOT NULL,
`rows` int NOT NULL,
`seatsPerRow` int NOT NULL,
`totalSeats` int NOT NULL,
`hasAisle` boolean DEFAULT true,
`description` text NULL
);
CREATE TABLE `tbl_bus_seat` (
`id` varchar(36) NOT NULL PRIMARY KEY,
`seatNumber` int NOT NULL,
`seatLabel` varchar(255) NULL,
`rowPosition` int NOT NULL,
`columnPosition` int NOT NULL,
`busTemplateId` varchar(36) NOT NULL,
`scheduleId` varchar(36) NULL,
`userId` varchar(36) NULL,
`status` varchar(50) NULL,
FOREIGN KEY (`busTemplateId`) REFERENCES `tbl_bus_template` (`id`),
FOREIGN KEY (`scheduleId`) REFERENCES `tbl_schedule` (`scheduleId`),
FOREIGN KEY (`userId`) REFERENCES `tbl_user` (`UUID`)
);
- URL:
/users/register
- Method:
POST
- 요청 본문:
{ "username": "홍길동", "email": "[email protected]", "password": "password123" }
- 응답:
{ "message": "회원가입이 완료되었습니다." }
- 설명: 새로운 사용자를 시스템에 등록합니다.
- URL:
/users/profile
- Method:
GET
- 인증 필요: 예 (액세스 토큰)
- 설명: 현재 로그인한 사용자의 프로필 정보를 반환합니다.
- URL:
/users/profile
- Method:
PUT
- 인증 필요: 예 (액세스 토큰)
- 요청 본문:
{ "username": "새이름", "email": "[email protected]", "password": "newpassword123" }
- 설명: 현재 로그인한 사용자의 정보를 수정합니다.
- URL:
/users/withdraw
- Method:
DELETE
- 인증 필요: 예 (액세스 토큰)
- 응답:
{ "message": "회원탈퇴가 완료되었습니다." }
- 설명: 현재 로그인한 사용자의 계정을 삭제합니다.
- URL:
/groups
- Method:
GET
- 인증 필요: 예 (액세스 토큰)
- 응답:
[ { "id": "group-id", "groupName": "수학 특강반", "isAdmin": true }, { "id": "group-id-2", "groupName": "상연 영어회화반", "isAdmin": false } ]
- 설명: 현재 로그인한 사용자가 속한 그룹 목록을 반환합니다. 각 그룹에 대해 해당 사용자가 관리자인지 여부(isAdmin)도 포함됩니다.
- URL:
/groups/:id
- Method:
GET
- 인증 필요: 예 (액세스 토큰)
- 응답:
{ "id": "group-id", "groupName": "수학 특강반", "isAdmin": true }
- 설명: 특정 ID를 가진 그룹 정보를 반환합니다. 현재 사용자가 이 그룹의 관리자인지 여부(isAdmin)도 포함됩니다.
- URL:
/groups
- Method:
POST
- 요청 본문:
{ "groupName": "수학 특강반" }
- URL:
/groups/:id
- Method:
PUT
- URL:
/groups/:id
- Method:
DELETE
- URL:
/group-members
- Method:
GET
- URL:
/group-members/group/:groupId
- Method:
GET
- URL:
/group-members/user/:userId
- Method:
GET
- URL:
/group-members
- Method:
POST
- 요청 본문:
{ "userId": "user-uuid", "groupId": "group-id", "isAdmin": false }
- URL:
/group-members/:userId/:groupId
- Method:
PUT
- URL:
/group-members/:userId/:groupId
- Method:
DELETE
- URL:
/schedules
- Method:
GET
- URL:
/schedules/:id
- Method:
GET
- URL:
/schedules/group/:groupId
- Method:
GET
- URL:
/schedules
- Method:
POST
- 요청 본문:
{ "groupId": "group-id", "scheduleName": "수학 시험", "location": "강의실 303호", "scheduleTime": "2025-04-15T14:00:00Z", "detailLocation": "공학관 3층", "detailLocation2": "좌측 복도 끝", "locationRange": "50" }
- URL:
/schedules/:id
- Method:
PUT
- URL:
/schedules/:id
- Method:
DELETE
- URL:
/attendances
- Method:
GET
- URL:
/attendances/:id
- Method:
GET
- URL:
/attendances/schedule/:scheduleId
- Method:
GET
- URL:
/attendances/user/:userId
- Method:
GET
- URL:
/attendances
- Method:
POST
- 요청 본문:
{ "scheduleId": "schedule-id", "userId": "user-id", "distanceFromLocation": 10.5, "attendanceTime": "2025-04-15T14:05:30Z" }
- URL:
/attendances/check
- Method:
POST
- 요청 본문:
{ "scheduleId": "schedule-id", "userId": "user-id", "distance": 10.5 }
- 설명: 사용자의 현재 위치를 기반으로 출석 상태를 자동으로 결정합니다.
- URL:
/attendances/:id
- Method:
PUT
- URL:
/attendances/:id
- Method:
DELETE
- URL:
/bus-templates
- Method:
GET
- 인증 필요: 예 (액세스 토큰)
- 설명: 등록된 모든 버스 템플릿 목록을 반환합니다.
- URL:
/bus-templates/:id
- Method:
GET
- 인증 필요: 예 (액세스 토큰)
- 설명: 특정 ID를 가진 버스 템플릿 정보를 반환합니다.
- URL:
/bus-templates
- Method:
POST
- 인증 필요: 예 (액세스 토큰, ADMIN 역할)
- 요청 본문:
{ "name": "1번 버스", "rows": 5, "seatsPerRow": 4, "totalSeats": 20, "hasAisle": true, "description": "소형 버스 레이아웃 - 통로 있음" }
- 설명: 새로운 버스 템플릿을 생성합니다.
- URL:
/bus-templates/:id
- Method:
PUT
- 인증 필요: 예 (액세스 토큰, ADMIN 역할)
- 설명: 특정 ID를 가진 버스 템플릿 정보를 수정합니다.
- URL:
/bus-templates/:id
- Method:
DELETE
- 인증 필요: 예 (액세스 토큰, ADMIN 역할)
- 설명: 특정 ID를 가진 버스 템플릿을 삭제합니다.
- URL:
/bus-seats
- Method:
GET
- 인증 필요: 예 (액세스 토큰)
- 쿼리 파라미터:
scheduleId
,busTemplateId
,userId
(선택적) - 설명: 조건에 따라 좌석 정보를 조회합니다. 모든 파라미터는 선택적입니다.
- URL:
/bus-seats/:id
- Method:
GET
- 인증 필요: 예 (액세스 토큰)
- 설명: 특정 ID를 가진 좌석 정보를 반환합니다.
- URL:
/bus-seats/user-schedule
- Method:
GET
- 인증 필요: 예 (액세스 토큰)
- 쿼리 파라미터:
userId
,scheduleId
(필수) - 설명: 특정 사용자가 특정 스케줄에서 배정받은 좌석 정보를 반환합니다.
- URL:
/bus-seats
- Method:
POST
- 인증 필요: 예 (액세스 토큰, ADMIN 역할)
- 요청 본문:
{ "seatNumber": 1, "seatLabel": "좌석 1", "rowPosition": 0, "columnPosition": 0, "busTemplateId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "scheduleId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "userId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "status": "empty" }
- 설명: 새로운 좌석을 생성합니다.
- URL:
/bus-seats/batch
- Method:
POST
- 인증 필요: 예 (액세스 토큰, ADMIN 역할)
- 요청 본문:
{ "busTemplateId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "scheduleId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "rows": 5, "seatsPerRow": 4, "hasAisle": true }
- 설명: 선택한 버스 템플릿과 스케줄에 대해 좌석을 일괄 생성합니다. 기존에 같은 템플릿과 스케줄 조합으로 생성된 좌석이 있다면 먼저 삭제합니다.
- URL:
/bus-seats/assign
- Method:
POST
- 인증 필요: 예 (액세스 토큰, ADMIN 역할)
- 요청 본문:
{ "userId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "seatId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "busTemplateId": "3fa85f64-5717-4562-b3fc-2c963f66afa6" }
- 설명: 특정 사용자를 특정 좌석에 배정합니다. 사용자가 이미 같은 스케줄에 다른 좌석을 배정받은 경우, 기존 좌석 배정은 해제됩니다.
- URL:
/bus-seats/:id
- Method:
PUT
- 인증 필요: 예 (액세스 토큰, ADMIN 역할)
- 설명: 좌석 정보를 수정합니다.
- URL:
/bus-seats/:id
- Method:
DELETE
- 인증 필요: 예 (액세스 토큰, ADMIN 역할)
- 설명: 특정 좌석을 삭제합니다.
- URL:
/bus-seats/schedule/:scheduleId
- Method:
DELETE
- 인증 필요: 예 (액세스 토큰, ADMIN 역할)
- 설명: 특정 스케줄에 배정된 모든 좌석을 삭제합니다.
- URL:
/bus-seats/bus-template/:busTemplateId
- Method:
DELETE
- 인증 필요: 예 (액세스 토큰, ADMIN 역할)
- 설명: 특정 버스 템플릿에 속한 모든 좌석을 삭제합니다.
- URL:
/auth/login
- Method:
POST
- 요청 본문:
{ "email": "[email protected]", "password": "password123" }
- 응답:
{ "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." }
- 설명: 이메일과 비밀번호로 로그인하고 JWT 토큰을 발급받습니다.
- URL:
/auth/logout
- Method:
GET
- 인증 필요: 예 (액세스 토큰)
- 응답:
{ "loggedOut": true }
- 설명: 현재 로그인한 사용자의 리프레시 토큰을 무효화합니다.
- URL:
/auth/refresh
- Method:
GET
- 인증 필요: 예 (리프레시 토큰)
- 응답:
{ "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." }
- 설명: 리프레시 토큰을 사용하여 새로운 액세스 토큰과 리프레시 토큰을 발급받습니다.
본 시스템은 JSON Web Token(JWT)을 사용한 인증 방식을 채택하고 있습니다.
- Access Token: 모든 API 요청에 사용되며, 기본 만료 기간은 7일입니다.
- Refresh Token: Access Token이 만료되었을 때 새로운 토큰을 발급받기 위해 사용되며, 만료 기간은 1년입니다.
- 사용자가 로그인 API(
/auth/login
)를 호출하여 인증 정보(이메일, 비밀번호)를 제출합니다. - 서버는 인증 정보를 검증하고, 유효한 경우 Access Token과 Refresh Token을 발급합니다.
- 클라이언트는 Access Token을 모든 인증이 필요한 API 요청의 헤더에 포함시켜야 합니다.
Authorization: Bearer {accessToken}
- Access Token이 만료되면, 클라이언트는 Refresh Token을 사용하여 새로운 토큰 쌍을 요청할 수 있습니다(
/auth/refresh
).
시스템은 다음과 같은 사용자 역할을 지원합니다:
- USER: 기본 사용자 역할로, 인증된 모든 사용자에게 부여됩니다.
- ADMIN: 특정 그룹의 관리자 역할로, 그룹 관련 관리 작업을 수행할 수 있습니다.
- TEACHER: 교사 역할 (추가 기능 구현 예정)
- STUDENT: 학생 역할 (추가 기능 구현 예정)
각 API 엔드포인트에는 특정 역할에 대한 접근 제한이 설정될 수 있으며, RolesGuard
를 통해 이를 강제합니다.
- 비밀번호 해싱: 모든 사용자 비밀번호는
bcrypt
를 사용하여 해싱되어 저장됩니다. - Refresh Token: 사용자 테이블에 해싱되어 저장되며, 로그아웃 시 무효화됩니다.
- JWT 시크릿 키: 환경 변수
JWT_ACCESS_SECRET
와JWT_REFRESH_SECRET
를 통해 설정할 수 있습니다.
프로젝트는 다음 계층으로 구성되어 있습니다:
-
도메인 계층: 핵심 비즈니스 로직 및 엔티티
- User, Group, GroupMember, Schedule, Attendance 등의 엔티티
- 도메인 관련 인터페이스
-
애플리케이션 계층: 유스케이스 구현
- 각 도메인의 서비스 클래스
- DTO 클래스
-
인프라 계층: 데이터베이스 접근 구현
- TypeORM 리포지토리
-
인터페이스 계층: API 컨트롤러