관리 메뉴

Mini

24.10.8. 개발일지 // 쿠키파서, db연결 , typeorm 본문

JS/boostCamp

24.10.8. 개발일지 // 쿠키파서, db연결 , typeorm

Mini_96 2024. 10. 8. 22:54

* 쿠키 파서 fix, MyCookieParser 구현

  • 현재 cookie parser가 작동을 안한다.
  • 라이브러리로 불러와서 내가 만든 was와 호환이 안되는것 같다.
  • -> 직접 만들자.

  • 역할 : cookie들을 파싱해서 req.cookies에 아래와 같은 식으로 넣어주면 되나?

이렇게 만드는 미들웨어를 만들자

  • MyCookieParser
import {Request} from "../was/request";
import {Response} from "../was/response";

/**
 * 쿠키 문자열을 파싱하여 객체로 변환합니다.
 * @param cookieString 쿠키 문자열
 * @returns 파싱된 쿠키 객체
 * {
 *   acookie: "dfdkfokeoef",
 *   bcookie: "afkifkjeife",
 *   ccookie: "asdsd"
 * }
 */
export function MyCookieParser(req : Request, res: Response, next)  {
    const cookies: { [key: string]: string } = {};

    const cookieString = req.get('cookie');

    // 쿠키 문자열을 세미콜론으로 분리
    const cookiePairs = cookieString.split(';');

    for (let pair of cookiePairs) {
        // 각 쌍을 트림하고 '='로 분리
        const [key, value] = pair.trim().split('=');

        // 키와 값이 모두 존재하는 경우에만 객체에 추가
        if (key && value) {
            cookies[key] = value;
        }
    }

    req.cookies = cookies;
    next();
}

파싱후
로그인시 멤버리스트 접근 성공!

 

* db연결 ,typeorm 설정

  • db연결, typeorm을 곁들인?
  • 인프런이 typeorm 쓴다고 해서 typeorm 사용하고자 함
  • 기업들이 많이 쓰는듯함
  • 우선 .env 파일만들고 npm install dotenv
  • main.ts에서 import
import "dotenv/config";
  • type orm 쓰려면 new Datasource를 생성해서 써야함
  • connection pool은 10개로 기본값설정됨.
import "reflect-metadata"
import { DataSource } from "typeorm"
import {Member} from "../domain/member/Member";

export const AppDataSource = new DataSource({
    type: process.env.DB_TYPE as "mysql" | "postgres" | "sqlite" | "mariadb",
    host: process.env.DB_HOST,
    port: parseInt(process.env.DB_PORT || "3306"),
    username: process.env.DB_USER,
    password: process.env.DB_PASSWORD,
    database: process.env.DB_NAME,
    synchronize: true,
    logging: true,
    entities: [Member],
    migrations: [],
    subscribers: [],
})
  • 그리고 npm install mysql2 해야함
  • 그리고 decorator 기반이기때문에 ts config 수정해야함
{
  "compilerOptions": {
    "module": "commonjs",
    "outDir": "dist",
    "sourceMap": true,
    "target": "ES6",
    "lib": ["es6"],
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true
  },
}
  • 엔티티 수정
import { Entity, PrimaryGeneratedColumn, Column } from "typeorm";

@Entity("members")
export class Member {

    @PrimaryGeneratedColumn()
    private id: number; // DB의 AutoIncrement Id

    @Column()
    private nickname: string;

    @Column()
    private readonly password: string;

    @Column({ unique: true })
    private readonly email: string;

    constructor(id: number, email : string , nickname: string, password : string) {
        this.id = id;
        this.nickname = nickname;
        this.password = password;
        this.email = email;
    }

    public setId(id: number) : void {
        this.id = id;
    }

    public getEmail(){
        return this.email;
    }
    public getId() {
        return this.id;
    }

    public getPassword() {
        return this.password;
    }
}
  • 메인에서 이렇게 사용하면 됨.
AppDataSource.initialize().
then(() => {
    console.log("Data Source has been initialized!")
}).catch((err) => {
    console.error("Error during Data Source initialization", err)
})

결과

  • 이제 memberRepository를 컨트롤러에서 주입받아서 사용?
  • 먼저 AppDataSource를 싱글톤으로 해야 한다. -> 안하면 컨트롤러마다 호출하는곳에서 새로운 DataSource를 만들면서 error가 난다. 그때그때마다 new Datasource를 해줘야한다.
import "reflect-metadata"
import { DataSource } from "typeorm"
import {Member} from "../domain/member/Member";

export class AppDataSource {
    private static instance: DataSource | null = null;

    private constructor() {}

    public static getInstance(): DataSource {
        if (!AppDataSource.instance) {
            AppDataSource.instance = new DataSource({
                type: process.env.DB_TYPE as "mysql" | "postgres" | "sqlite" | "mariadb",
                host: process.env.DB_HOST,
                port: parseInt(process.env.DB_PORT || "3306"),
                username: process.env.DB_USER,
                password: process.env.DB_PASSWORD,
                database: process.env.DB_NAME,
                synchronize: true,
                logging: true,
                entities: [Member],
                migrations: [],
                subscribers: [],
            });
        }
        return AppDataSource.instance;
    }
}
  • memberRepository
import { Member } from "./Member";
import { AppDataSource } from "../../repositories/AppDataSource";
import { Repository } from "typeorm";

export class MemberRepository {
    private static instance: MemberRepository | null = null;
    private memberRepository: Repository<Member>;

    private constructor() {
        this.memberRepository = AppDataSource.getInstance().getRepository(Member);
    }

    /**
     * MemberRepository의 유일한 인스턴스를 반환합니다.
     * 인스턴스가 없으면 새로 생성하여 반환합니다.
     * @returns {MemberRepository} MemberRepository의 유일한 인스턴스
     */
    public static getInstance(): MemberRepository {
        if (MemberRepository.instance === null) {
            MemberRepository.instance = new MemberRepository();
        }
        return MemberRepository.instance;
    }

    /**
     * 새로운 Member를 저장소에 추가합니다.
     * @param {Member} member - 저장할 Member 객체
     * @returns {Promise<Member>} 저장된 Member
     */
    public async save(member: Member): Promise<Member> {
        try {
            return await this.memberRepository.save(member);
        } catch (error) {
            if (error.code === 'ER_DUP_ENTRY') {
                console.log('이미 존재하는 email 입니다.' + member.getEmail());
                throw new Error('이미 존재하는 email 입니다.' + member.getEmail());
            }
            throw error;
        }
    }

    /**
     * Email로 Member를 조회합니다.
     * @param {string} email - 조회할 Member의 Email
     * @returns {Promise<Member | null>} 조회된 Member 객체 또는 null
     */
    public async findByEmail(email: string): Promise<Member | null> {
        return await this.memberRepository.findOne({ where: { email } });
    }

    public async findById(id: number): Promise<Member | null> {
        return await this.memberRepository.findOneBy({ id });
    }

    /**
     * 모든 Member를 반환합니다.
     * @returns {Promise<Member[]>} 저장된 모든 Member 객체의 배열
     */
    public async findAll(): Promise<Member[]> {
        return await this.memberRepository.find();
    }
}
  • 를 사용하는 컨트롤러 -> 추후에 로직이 복잡해지면 서비스 레이어로 분리
import {Member} from "../../../domain/member/Member";
import {MemoryMemberRepository} from "../../../domain/member/MemoryMemberRepository";
import {ControllerV4} from "../ControllerV4";
import {MemberRepository} from "../../../domain/member/MemberRepository";
import {AppDataSource} from "../../../repositories/AppDataSource";

export class MemberSaveControllerV4 implements ControllerV4{

    private memberRepository: MemberRepository;

    constructor() {
        this.memberRepository = MemberRepository.getInstance();
    }

    process(paramMap: Map<string, string>, model: Map<string, object>):string {
        const email: string = paramMap.get('email');
        const nickname: string = paramMap.get('nickname');
        const password: string = paramMap.get('password');

        const member = new Member(email, nickname , password);
        this.memberRepository.save(member);

        model.set("member", member);
        return "save-result";
    }
}

 

* 에러 핸들 문제

  • 현재 중복된 email로 가입하는경우, email 컬럼에 unique 제약조건으로 인해 error 가 난다.
  • 이를 catch 해주는 부분이 없어서 main 까지 error 가 전파되서 서버가 죽는듯하다.
  • 일단 임시로 

  • 일단 error를 더이상 던지지 않도록하여 임시적으로 해결.
/**
 * 새로운 Member를 저장소에 추가합니다.
 * @param {Member} member - 저장할 Member 객체
 * @returns {Promise<Member>} 저장된 Member
 */
public async save(member: Member): Promise<Member> {
    try {
        return await this.memberRepository.save(member);
    } catch (error) {
        if (error.code === 'ER_DUP_ENTRY') {
            console.log('이미 존재하는 email 입니다.' + member.getEmail());
            // throw new Error('이미 존재하는 email 입니다.' + member.getEmail());
        }
        // throw error;
    }
}

 

* login 구현 with db

  • db에서 결과가 오기를 기다려야한다.

이를 위해서 async , await을 붙여줘야한다.
이를 호출하는 부분에서도 async await 가 전파되야한다.

  • await을 안붙이면 promise 객체가 되어 작동하지 않는다.
  • async await 가 전파되는 문제... service함수를 호출하는 곳이 또있고 계속 호출 호출... 되서 100번 호출된다면 100곳에 모두 이작업을 반복해야 하는가?
  • 수정문제 : 어떤 함수가 await 기능이 필요할수있으니 항상 async를 붙여 놓는게 나은가? 필요할때 그때그때 붙이는게 나은가?

 

* 회원리스트 구현 with db

  • 멤버 엔티에 컬럼추가

@CreateDateColumn() //밀리초 저장안함.
createdAt: Date;

@UpdateDateColumn()
updatedAt: Date;
  • 컨트롤러의 저장소를 db저장소로 변경
  • await 달기
import {ControllerV4} from "../ControllerV4";
import {MemoryMemberRepository} from "../../../domain/member/MemoryMemberRepository";
import {MemberRepository} from "../../../domain/member/MemberRepository";

export class UserListControllerV4 implements ControllerV4{

    private memberRepository: MemberRepository = MemberRepository.getInstance();

    async process(paramMap: Map<string, string>, model: Map<string, object>) {
        const members = await this.memberRepository.findAll();

        model.set("members",members);

        return "user/list";
    }
}
  • 를 호출하는 adapter handle에도 await 달기

결과

  • 뭔가 정렬이 맘대로 안된다.
  • 앞의 * 표시도 없애고 싶다.
ul {
    list-style-type: none;
}
<section class="board-box">
    <p class="board-count">전체멤버<span>N</span>명</p>
    <ul class="board-list" >
        <li class="board-header">
            <p class="board-header-nickname BordS">닉네임</p>
            <p class="board-header-email BordS">이메일</p>
            <p class="board-header-createdAt BordS">회원가입일</p>
        </li>
        <% members.forEach(function(member) { %>
            <li class="board-item">
                <p class="board-header-nickname BordS"><%= member.nickname %></p>
                <p class="board-header-email BordS"><%= member.email %></p>
                <p class="board-header-createdAt BordS"><%= member.createdAt %></p>
            </li>
        <% }); %>
    </ul>
</body>
ul {
    list-style-type: none;
}

.board-list {
    list-style-type: none;
    padding: 0;
    display: grid;
    grid-template-columns: 1fr 2fr 1fr;  /* 닉네임, 이메일, 회원가입일의 비율 */
    gap: 10px;  /* 열 사이의 간격 */
}

.board-header, .board-item {
    display: contents;
}

.board-header-nickname,
.board-header-email,
.board-header-createdAt {
    padding: 10px;
    text-align: left;
}

.board-header p {
    font-weight: bold;
    background-color: #f0f0f0;  /* 헤더 배경색 */
}

.board-item p {
    background-color: #ffffff;  /* 아이템 배경색 */
}

/* 짝수 번째 아이템에 다른 배경색 적용 (선택사항) */
.board-item:nth-child(even) p {
    background-color: #f9f9f9;
}

/* 반응형 디자인을 위한 미디어 쿼리 */
@media (max-width: 768px) {
    .board-list {
        grid-template-columns: 1fr;
    }

    .board-header {
        display: none;  /* 모바일에서는 헤더를 숨김 */
    }

    .board-item p {
        padding: 5px;
    }

    .board-item {
        display: grid;
        grid-template-columns: 1fr;
        gap: 5px;
        margin-bottom: 10px;
    }
}

뭔가 요구사항에는 안맞지만 괜찮네