PROJECTπŸ’»

[iOS] ν•œμ„±λŒ€ κΈ°μˆ™μ‚¬ μ–΄ν”Œ ν”„λ‘œμ νŠΈ

쏴리쏭 2025. 2. 13. 00:18

ν•΄λ‹Ή ν”„λ‘œμ νŠΈ GitHub Repository

https://github.com/HansungDomitory/HansungDomitory_Backend

 

GitHub - HansungDomitory/HansungDomitory_Backend

Contribute to HansungDomitory/HansungDomitory_Backend development by creating an account on GitHub.

github.com

 

1. 주제 μ„€λͺ…

 ν•œμ„±λŒ€ν•™κ΅ 여름방학 μ§„λ‘œνƒμƒ‰ν•™μ μ œ ν”„λ‘œμ νŠΈμž…λ‹ˆλ‹€.

ν•™κ΅μ—μ„œ λ°©ν•™λ™μ•ˆ ν•˜λŠ” νŠΉλ³„ν•œ ν™œλ™μΈ 만큼 이 κΈ°κ°„ λ™μ•ˆ κΌ­ λ§Œλ“€μ–΄λ³΄κ³  μ‹Άμ—ˆμœΌλ©΄μ„œλ„ 학ꡐ에도 도움이 될 것 같은 주제둜 ν•˜κ³  μ‹Άλ‹€λŠ” 생각이 λ“€μ—ˆμŠ΅λ‹ˆλ‹€.

κ·ΈλŸ¬λ‹€κ°€ 학ꡐ κΈ°μˆ™μ‚¬ μ–΄ν”Œλ¦¬μΌ€μ΄μ…˜μ„ μ‚¬μš©ν•˜λŠ”λ° λΆˆνŽΈν•œ 점이 κ½€ λ§Žμ•˜λ˜ 기얡이 μžˆμ–΄ 이λ₯Ό λͺ¨ν‹°λΈŒν•˜λ©΄ 쒋을 것 κ°™μ•„ 주제λ₯Ό ν•œμ„±λŒ€ κΈ°μˆ™μ‚¬ μ–΄ν”Œλ‘œ μ„ μ •ν•˜κ²Œ λ˜μ—ˆμŠ΅λ‹ˆλ‹€.


2. 개발 κΈ°κ°„

2024.07.04 ~ 2024.08.05 (μ•½ 1κ°œμ›”)


3. μ—­ν• 

λ°±μ—”λ“œ, 배포 μ„œλ²„ 관리
  • 섀계
    • ERD 섀계
    • 둜그인 κ³Όμ • 섀계
  • κ΅¬ν˜„
    • 둜그인 κΈ°λŠ₯ κ΅¬ν˜„
    • μ™Έλ°•μ‹ μ²­ κΈ°λŠ₯ κ΅¬ν˜„
    • μƒλ²Œμ  κΈ°λŠ₯ κ΅¬ν˜„
    • 곡지사항 κΈ°λŠ₯ κ΅¬ν˜„
  • 배포
    • AWS EC2 μΈμŠ€ν„΄μŠ€ 배포

4. 기술 μŠ€νƒ

  • μ–Έμ–΄: TypeScript
  • ν”„λ ˆμž„μ›Œν¬: NestJS
  • λ°μ΄ν„°λ² μ΄μŠ€: MySQL
  • 배포: AWS EC2
  • 도ꡬ: Docker, RestAPI, Swagger

5. ERD


6. 전체 ꡬ쑰


7. 상세 κ΅¬ν˜„ λ‚΄μš©

NestJS ν™˜κ²½μ—μ„œ RestAPI둜 λ°±μ—”λ“œ APIλ₯Ό κ΅¬ν˜„ν–ˆμŠ΅λ‹ˆλ‹€.

7-1. νšŒμ› 관리

  • JWT 토큰을 ν™œμš©ν•œ 둜그인 관리

JWT 토큰을 λ§Œλ“€μ–΄μ„œ ν•™μƒμ˜ λ‘œκ·ΈμΈμ„ κ΄€λ¦¬ν–ˆμŠ΅λ‹ˆλ‹€.

JWT 토큰은 둜그인 처리λ₯Ό λ‹΄λ‹Ήν•˜λŠ” accessTokenκ³Ό accessToken κΈ°κ°„ 만료 μ‹œ μž¬λ°œκΈ‰μ„ λ‹΄λ‹Ήν•˜λŠ” refreshToken으둜 λ§Œλ“€μ—ˆμŠ΅λ‹ˆλ‹€.

refreshToken은 Cookieλ₯Ό 톡해 ν΄λΌμ΄μ–ΈνŠΈμ— μ „λ‹¬ν•˜λŠ” 방식을 μ‚¬μš©ν•˜μ˜€μŠ΅λ‹ˆλ‹€.

 

[accessToken λ°œκΈ‰]

getAccessToken(student: Student | IStudentContext['student']): string {
    return this.jwtService.sign(
        { sub: student.id },
        { secret: process.env.JWT_ACCESS_SECRET, expiresIn: '100m' }
    );
}

 

[refreshToken λ°œκΈ‰]

getRefreshToken(student: Student, context: IContext): void {
    const refreshToken = this.jwtService.sign(
        { sub: student.id },
        { secret: process.env.JWT_REFRESH_SECRET, expiresIn: '2w' },
    );

    context.res.setHeader(
        'set-cookie',
        `refreshToken=${refreshToken}; path=/;`,
    );
}

 

  • μ•ˆμ •μ μΈ λΉ„λ°€λ²ˆν˜Έ 관리

학생이 νšŒμ›κ°€μž…μ„ ν•  λ•Œ ν•™λ²ˆκ³Ό λΉ„λ°€λ²ˆν˜Έλ₯Ό μž…λ ₯ν•˜κΈ° λ•Œλ¬Έμ— λΉ„λ°€λ²ˆν˜Έλ₯Ό λ°μ΄ν„°λ² μ΄μŠ€μ— μ•ˆμ •μ μœΌλ‘œ μ €μž₯ν•˜λŠ” 과정이 ν•„μš”ν–ˆμŠ΅λ‹ˆλ‹€.

bcrypt의 hash κΈ°λŠ₯을 μ‚¬μš©ν•΄μ„œ λΉ„λ°€λ²ˆν˜Έλ₯Ό hash μ²˜λ¦¬ν•œ ν›„ μ €μž₯ν•˜μ˜€κ³ , λΉ„λ°€λ²ˆν˜Έ 확인 μ‹œμ—λŠ” hash 된 λΉ„λ°€λ²ˆν˜Έλ₯Ό λ³΅ν˜Έν™”ν•˜μ—¬ λ§€μΉ­μ‹œμΌ°μŠ΅λ‹ˆλ‹€.

 

[νšŒμ›κ°€μž… μ‹œ λΉ„λ°€λ²ˆν˜Έ hash 처리]

async create({ createStudentInput }: IStudentServiceCreate): Promise<Student> {
    
    ...
    const { password, ...rest} = createStudentInput;
    const hashedPassword = await bcrypt.hash(password, 10);
    ...
}

 

 

7-2. κ΄€λ¦¬μž μ„€μ •

μƒλ²Œμ  관리와 곡지사항 μž‘μ„±μ€ κ΄€λ¦¬μžλ§Œ μ΄μš©ν•  수 μžˆμ–΄μ•Ό ν•˜κΈ° λ•Œλ¬Έμ— νšŒμ›κ°€μž… μ‹œ μ‘΄μž¬ν•  수 μ—†λŠ” ν•™λ²ˆμΈ '000000'을 κ°€μ§„ μ‚¬μš©μžλ₯Ό κ΄€λ¦¬μžλ‘œ λ“±λ‘ν–ˆμŠ΅λ‹ˆλ‹€.

그리고, 정상적인 경둜둜 λ§Œλ“€ 수 μ—†λŠ” κ΄€λ¦¬μž 계정은 λ°μ΄ν„°λ² μ΄μŠ€μ—μ„œ 직접 ν•™λ²ˆμ„ μˆ˜μ •ν•˜μ—¬ λ§Œλ“€μ—ˆμŠ΅λ‹ˆλ‹€.

 

[νšŒμ›κ°€μž… μ‹œ κ΄€λ¦¬μž 계정 생성 λ°©μ§€]

async create({ createStudentInput }: IStudentServiceCreate): Promise<Student> {

    ...
    if(createStudentInput.id === '0000000') {
        throw new BadRequestException('νšŒμ›κ°€μž…ν•  수 μ—†λŠ” IDμž…λ‹ˆλ‹€.');
    }
    …
}

 

κ΄€λ¦¬μžλ§Œ ν˜ΈμΆœν•  수 μžˆλŠ” API에 ν˜ΈμΆœν•œ μ‚¬μš©μžκ°€ κ΄€λ¦¬μžμΈμ§€ ν™•μΈν•˜λŠ” μž‘μ—…μ„ μΆ”κ°€ν•˜κ³ , κ΄€λ¦¬μžκ°€ 아닐 μ‹œμ— ForbiddenException μ˜ˆμ™Έλ₯Ό λ°œμƒμ‹œμΌœμ„œ 일반 μ‚¬μš©μžκ°€ μ‚¬μš©ν•  수 없도둝 κ΅¬ν˜„ν–ˆμŠ΅λ‹ˆλ‹€.

 

[μƒλ²Œμ  μΆ”κ°€ APIμ—μ„œ κ΄€λ¦¬μž 확인 절차 μΆ”κ°€]

async create(myId: string, createScoreRecordInput: CreateScoreRecordInput) {
    
    if(!myId || myId !== '0000000') {
        throw new ForbiddenException('κ΄€λ¦¬μžλ§Œ μƒλ²Œμ  기둝을 생성할 수 μžˆμŠ΅λ‹ˆλ‹€.');
    }
    …
}

 

 

7-3. 랜덀 λ°© λ°°μ •

κΈ°μˆ™μ‚¬ μ–΄ν”Œλ¦¬μΌ€μ΄μ…˜μ— κ°€μž…ν•œ 학생듀은 각자 빈 λ°© λ²ˆν˜Έκ°€ 배정이 λ˜μ–΄μ•Ό ν•©λ‹ˆλ‹€.

λ”°λΌμ„œ, 배정이 λ˜μ§€ μ•Šμ€ 빈 방을 μ°ΎλŠ” κ³Όμ •κ³Ό λ°°μ •λ˜μ§€ μ•Šμ€ 빈 방을 νšŒμ› κ°€μž…ν•œ ν•™μƒμ—κ²Œ λ°°μ •ν•˜λŠ” 과정을 κ΅¬ν˜„ν–ˆμŠ΅λ‹ˆλ‹€.

 

[랜덀 λ°© 번호 생성]

private generateRandomRoom(): string {
    const floor = this.getRandomInt(1, 9);
    const roomSuffix = this.getRandomInt(1, 9);
    const tailDasi = this.getRandomInt(1, 2);
    return `${floor}0${roomSuffix}-${tailDasi}`;
}

 

[μ‚¬μš©λ˜μ§€ μ•Šμ€ 랜덀 λ°© 번호 λ°°μ •]

private async assignRandomRoom(): Promise<string> {
    const existingRooms = await this.studentRepository.find({ select: ['room'] });
    const existingRoomSet = new Set(existingRooms.map(student => student.room));

    while (true) {
        const randomRoom = this.generateRandomRoom();
        if (!existingRoomSet.has(randomRoom)) {
            return randomRoom;
        }
    }
}

 

 

7-4. μƒλ²Œμ  관리

μƒλ²Œμ μ€ κ΄€λ¦¬μžκ°€ ν•™μƒμ—κ²Œ λΆ€μ—¬ν•˜λŠ” λ°©μ‹μœΌλ‘œ κ΅¬ν˜„ν–ˆμŠ΅λ‹ˆλ‹€.

상점과 λ²Œμ μ€ λ”°λ‘œ κ΅¬ν˜„ν•˜μ§€ μ•Šκ³ , ν•˜λ‚˜μ˜ 점수 ν•„λ“œλ‘œ μƒμ„±ν•œ ν›„ μ–‘μˆ˜λŠ” μƒμ μœΌλ‘œ, μŒμˆ˜λŠ” 벌점으둜 κ΅¬ν˜„ν–ˆμŠ΅λ‹ˆλ‹€.

ν•˜λ‚˜μ˜ ν•„λ“œλ‘œλ§Œ κ΅¬ν˜„ν•œ 이유

상점이 μžˆλŠ” μƒνƒœμ—μ„œ 벌점이 λΆ€μ—¬λ˜λ©΄ 그만큼 상점이 μ°¨κ°λ˜λŠ” κ²ƒμ²˜λŸΌ 상점과 λ²Œμ μ€ μ„œλ‘œ μ—°κ΄€λ˜μ–΄ μžˆλŠ” μƒνƒœμ΄κΈ° λ•Œλ¬Έμ—, 상점과 λ²Œμ μ„ λ”°λ‘œ λ‚˜λˆ μ„œ κ΅¬ν˜„ν•˜μ§€ μ•Šκ³  κ°„λ‹¨ν•˜κ²Œ μ μˆ˜λΌλŠ” ν•˜λ‚˜μ˜ ν•„λ“œλ§Œ κ°–κ²Œ ν–ˆμŠ΅λ‹ˆλ‹€.

 

[점수 ν•„λ“œ ν•˜λ‚˜λ‘œλ§Œ κ΅¬ν˜„ν•œ μƒλ²Œμ  κΈ°λŠ₯ μ—”ν‹°ν‹°]

@Entity()
export class ScoreRecord {
    @ApiProperty({ description: "고유번호" })
    @PrimaryGeneratedColumn('increment')
    id: number;

    @ApiProperty({ description: "ν•΄λ‹Ή 기둝 κ΄€λ ¨ 학생" })
    @ManyToOne(() => Student, { onDelete: 'CASCADE' })
    @JoinColumn()
    student: Student;

    @ApiProperty({ description: "상점/벌점 μ—¬λΆ€" })
    @Column()
    is_merit: boolean;

    @ApiProperty({ description: "점수" })
    @Column()
    score: number;

    @ApiProperty({ description: "μƒμ„Έλ‚΄μš©" })
    @Column()
    detail: string;

    @ApiProperty({ description: "μƒμ„±μΌμž" })
    @CreateDateColumn()
    create_at: Date;
}

8. μ‹€ν–‰ μ‹œλ‚˜λ¦¬μ˜€

8-1. νšŒμ›κ°€μž…, 둜그인

  • νšŒμ›κ°€μž…: 이름, ν•™λ²ˆ, λΉ„λ°€λ²ˆν˜Έλ₯Ό λ‹€ μž…λ ₯ν•΄μ•Ό νšŒμ›κ°€μž…μ΄ κ°€λŠ₯ν•©λ‹ˆλ‹€.
    • νšŒμ›κ°€μž…μ„ ν•˜λ©΄ 랜덀으둜 κΈ°μˆ™μ‚¬ 빈 방이 λ°°μ •λ©λ‹ˆλ‹€.
  • 둜그인: νšŒμ›κ°€μž… ν›„, ν•™λ²ˆκ³Ό λΉ„λ°€λ²ˆν˜Έλ₯Ό μž…λ ₯ν•˜λ©΄ 둜그인이 λ©λ‹ˆλ‹€.

8-2. ν™ˆ ν™”λ©΄ 및 κΈ°μˆ™μ‚¬ μ†Œκ°œ ν™”λ©΄

  • ν™ˆ ν™”λ©΄: 메인 ν™”λ©΄μœΌλ‘œ, ν•΄λ‹Ή νŽ˜μ΄μ§€μ—μ„œ κΈ°μˆ™μ‚¬ μ†Œκ°œ νŽ˜μ΄μ§€μ™€ 곡지사항 νŽ˜μ΄μ§€λ‘œ 이동할 수 μžˆμŠ΅λ‹ˆλ‹€.
  • κΈ°μˆ™μ‚¬ μ†Œκ°œ ν™”λ©΄: ν•œμ„±λŒ€ν•™κ΅ κΈ°μˆ™μ‚¬μ— κ΄€ν•œ 정보가 λ‚˜μ™€μžˆμŠ΅λ‹ˆλ‹€.

8-3. μ™Έλ°•μ‹ μ²­

  • 이 νŽ˜μ΄μ§€μ—μ„œ μ™Έλ°• 신청이 κ°€λŠ₯ν•˜λ©°, ν•™λ²ˆκ³Ό 이름은 μžλ™μœΌλ‘œ λΆˆλŸ¬μ™€μ§€κ³  λ‚ μ§œλ₯Ό μ„ νƒν•˜λ©΄ μžλ™μœΌλ‘œ μ™Έλ°•μΌμˆ˜κ°€ λ³΄μ—¬μ§‘λ‹ˆλ‹€.
  • λ‚ μ§œλ₯Ό μ„ νƒν•˜κ³  λ‚΄μš©κΉŒμ§€ μ μ–΄μ„œ μ‹ μ²­ν•˜κΈ° λ²„νŠΌμ„ λˆ„λ₯΄λ©΄ μ™Έλ°• ν˜„ν™© νŽ˜μ΄μ§€κ°€ λ‚˜μ˜€κ³ , κ·Έ νŽ˜μ΄μ§€μ—μ„œ κΈ°κ°„κ³Ό μ™Έλ°•μΌμˆ˜, λ“±λ‘μΌμžλ₯Ό 확인할 수 μžˆμŠ΅λ‹ˆλ‹€.
  • μ™Έλ°• μ‹ μ²­ν–ˆλ˜ λͺ©λ‘λ“€μ€ λ“±λ‘μΌμž κΈ°μ€€μœΌλ‘œ λ‚΄λ¦Όμ°¨μˆœμœΌλ‘œ λ°°μ—΄λ˜μ–΄ 있으며, μ™Έλ°• 기간이 되기 μ „μ—λŠ” μˆ˜μ •κ³Ό μ‚­μ œκ°€ κ°€λŠ₯ν•©λ‹ˆλ‹€.

 

8-4. λ§ˆμ΄νŽ˜μ΄μ§€, μƒλ²Œμ  쑰회

  • λ§ˆμ΄νŽ˜μ΄μ§€
    • 학생 이름, λ°°μ •λœ 방이 λ‚˜μ™€ 있으며, ν•΄λ‹Ή νŽ˜μ΄μ§€μ—μ„œ λ‘œκ·Έμ•„μ›ƒ λ²„νŠΌμ„ λˆ„λ₯΄λ©΄ λ‘œκ·Έμ•„μ›ƒμ΄ κ°€λŠ₯ν•©λ‹ˆλ‹€.
    • ν•΄λ‹Ή νŽ˜μ΄μ§€μ—μ„œ 곡지사항 νŽ˜μ΄μ§€, μ™Έλ°•ν˜„ν™© νŽ˜μ΄μ§€, μƒλ²Œμ ν˜„ν™© νŽ˜μ΄μ§€λ‘œ 이동할 수 μžˆμŠ΅λ‹ˆλ‹€.
  • μƒλ²Œμ  쑰회
    • 학생 정보와 μƒλ²Œμ  점수, 총점, μƒλ²Œμ μ— κ΄€ν•œ μ‚¬μœ μ™€ μ μš©μΌμžκ°€ λ‚˜μ™€μžˆκ³  λ‚΄λ¦Όμ°¨μˆœμœΌλ‘œ λ‚˜μ—΄λ˜μ–΄ μžˆμŠ΅λ‹ˆλ‹€.
    • νŒŒλž€μƒ‰ +μ μˆ˜λŠ” κΈ°μ‘΄ μ μˆ˜μ—μ„œ κ°€μ‚°λ˜κ³ , 빨간색 -μ μˆ˜λŠ” κΈ°μ‘΄ μ μˆ˜μ—μ„œ κ°μ‚°λ©λ‹ˆλ‹€.

8-5. 곡지사항

  • κ΄€λ¦¬μžμΈ‘μ—μ„œ κΈ°μˆ™μ‚¬μ™€ κ΄€λ ¨λœ 곡지사항을 올리면 학생듀은 이 곡지사항듀을 확인할 수 μžˆμŠ΅λ‹ˆλ‹€.
  • 곡지사항은 κ°€μž₯ 였래된 κΈ€λΆ€ν„° λ‚΄λ¦Όμ°¨μˆœμœΌλ‘œ 올렀져 있고, 학생듀은 검색을 톡해 κΆκΈˆν•œ 곡지 사항을 λ³Ό 수 μžˆμŠ΅λ‹ˆλ‹€.