<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>근본없는 지식창고</title>
    <link>https://nbkw.tistory.com/</link>
    <description></description>
    <language>ko</language>
    <pubDate>Sun, 23 Aug 2026 17:48:25 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>haaak</managingEditor>
    <item>
      <title>[wanted-pre-onboarding] Assignment7</title>
      <link>https://nbkw.tistory.com/204</link>
      <description>&lt;h1&gt;Github Repo&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/yhshin0/cardoc-subject&quot;&gt;https://github.com/yhshin0/cardoc-subject&lt;/a&gt;&lt;/p&gt;
&lt;h1&gt;개요&lt;/h1&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;타이어 API 설계 및 구현&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;기간&lt;/h1&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;2021.11.22 - 2021.11.27&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;개발 환경&lt;/h1&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;언어 : TypeScript&lt;/li&gt;
&lt;li&gt;프레임워크 : NestJS&lt;/li&gt;
&lt;li&gt;DB : SQLite3&lt;/li&gt;
&lt;li&gt;라이브러리 : axios, passport, jwt, bcrypt, typeOrm, class-validator&lt;/li&gt;
&lt;li&gt;배포 환경 : heroku&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;구현 사항&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요구사항&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;RDB 사용&lt;/li&gt;
&lt;li&gt;실행 방법 서술&lt;/li&gt;
&lt;li&gt;ORM 사용&lt;/li&gt;
&lt;li&gt;response status(200 OK: 성공, 400 Bad Request: 잘못된 파라미터, 401 Unauthorized: 인증 헤더 오류, 500 Internal Server Error: 기타 서버 에러) 반환&lt;/li&gt;
&lt;li&gt;사용자 생성 API
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ID / Password로 사용자 생성 구현&lt;/li&gt;
&lt;li&gt;ID / Password로 로그인 구현&lt;/li&gt;
&lt;li&gt;인증 토큰 발급&lt;/li&gt;
&lt;li&gt;인증 토큰으로 인증된 사용자만 API 호출 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;사용자가 소유한 타이어 정보 저장 API
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;자동차 차종 ID(trimID)를 사용하여 사용자가 소유한 자동차 정보 저장.&lt;/li&gt;
&lt;li&gt;타이어 정보 저장. 타이어 정보는 &lt;code&gt;자동차 정보 API(trimID) -&amp;gt; spec -&amp;gt; driving -&amp;gt; frontTire/rearTire&lt;/code&gt; 에 있음&lt;/li&gt;
&lt;li&gt;한번에 최대 5명까지의 사용자에 대한 요청을 받을 수 있음. 즉, 사용자 정보와 trimID 5쌍을 요청데이터로 하여 API를 호출할 수 있음
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;5명이 넘어가는 요청에 적당한 에러 처리&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;{폭}/{편평비}R{휠 사이즈}&lt;/code&gt;과 같은 형식의 데이터일 경우만 DB에 항목별로 나누어 서로 다른 Column에 저장&lt;br /&gt;예: &lt;code&gt;205/75R18&lt;/code&gt; -&amp;gt; 타이어 폭: 205, 편평비: 75, 휠 사이즈: 18&lt;/li&gt;
&lt;li&gt;유효하지 않은 trimID 일 경우 에러 처리(유효하지 않은 trimID로 자동차 정보 API를 호출하면 &lt;code&gt;{&quot;code&quot;:-1000,&quot;message&quot;:&quot;No value present&quot;}&lt;/code&gt; 라는 response를 받음)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;사용자 소유 타이어 정보 조회 API
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;사용자 ID를 통해 저장한 타이어 정보를 조회할 수 있음&lt;/li&gt;
&lt;li&gt;페이지네이션
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;page, pageSize, totalCount(response) 사용&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;DB 모델링&lt;/h1&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1181&quot; data-origin-height=&quot;453&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mP2tj/btrmksVleBz/jWvjS6IlEA41JfJ6IeNzyK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mP2tj/btrmksVleBz/jWvjS6IlEA41JfJ6IeNzyK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mP2tj/btrmksVleBz/jWvjS6IlEA41JfJ6IeNzyK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmP2tj%2FbtrmksVleBz%2FjWvjS6IlEA41JfJ6IeNzyK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1181&quot; height=&quot;453&quot; data-origin-width=&quot;1181&quot; data-origin-height=&quot;453&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h1&gt;AuthModule에서 env 값 사용하기&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AuthModule에서 JwtModule을 사용하려면 register를 통해 등록해야 하는데, 단순 register() 만으로는 ConfigModule이 읽는 .env 파일의 값을 사용할 수 없습니다.&lt;br /&gt;따라서 registerAsync()를 사용하여 .env 파일의 값을 직접 넣어줘야 합니다.&lt;br /&gt;이를 위해서는 AuthModule에서 ConfigModule을 import 하고, registerAsync() 에서도 import 하여 ConfigService를 사용할 수 있도록 의존성을 주입해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;auth.module.ts&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@Module({
  imports: [
    UsersModule,
    PassportModule,
    ConfigModule,
    JwtModule.registerAsync({
      imports: [ConfigModule],
      inject: [ConfigService],
      useFactory: async (configService: ConfigService) =&amp;gt; ({
        secret: configService.get('JWT_SECRET'),
        signOptions: { expiresIn: configService.get('JWT_EXPIRATION') },
      }),
    }),
  ],&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고: &lt;a href=&quot;https://velog.io/@algo2000/pj01-11&quot;&gt;https://velog.io/@algo2000/pj01-11&lt;/a&gt;&lt;br /&gt;&lt;a href=&quot;https://docs.nestjs.kr/techniques/configuration&quot;&gt;https://docs.nestjs.kr/techniques/configuration&lt;/a&gt;&lt;/p&gt;
&lt;h1&gt;private method mocking&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;private method는 mocking한 클래스에서 바로 사용하기 어렵습니다. 즉, &lt;code&gt;jest.spyOn(tiresService, 'getTireInfoFromAPI')&lt;/code&gt; 과 같이 코드를 작성할 수 없습니다.&lt;br /&gt;따라서 이를 해결하기 위해 mocking 하려는 메소드가 있는 클래스의 prototype에서 private 메소드가 실행되는지 확인해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;tires.service.spec.ts&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;      ...

      jest
        .spyOn(TiresService.prototype as any, 'getTireInfoFromAPI')
        .mockResolvedValue(validAPIResult);

      ...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고: &lt;a href=&quot;https://stackoverflow.com/questions/56044471/testing-private-functions-in-typescript-with-jest&quot;&gt;https://stackoverflow.com/questions/56044471/testing-private-functions-in-typescript-with-jest&lt;/a&gt;&lt;/p&gt;
&lt;h1&gt;createQueryBuilder mocking&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;typeOrm의 createQueryBuilder를 통해 DB에서 데이터를 가져오는 경우 메소드 체이닝이 되어 있기 때문에 mocking 역시 그에 맞게 코드를 작성해야 합니다.&lt;br /&gt;테스트 코드에서 객체를 작성하고 체이닝 된 메소드가 호출될 때 해당 객체가 반환될 수 있도록 객체를 정의합니다.&lt;br /&gt;그리고 마지막 체이닝 메소드에 원하는 결과를 반환하도록 작성한 뒤, mockImplementation() 메소드를 통해 해당 결과가 나올 수 있도록 구현합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;tires.service.spec.ts&lt;/p&gt;
&lt;pre class=&quot;coffeescript&quot;&gt;&lt;code&gt;const mockTiresRepository = () =&amp;gt; ({
  save: jest.fn(),
  createQueryBuilder: jest.fn(),
});
...
    it('유저 아이디를 통해 타이어 정보 조회에 성공한다', async () =&amp;gt; {
      ...

      const createQueryBuilder = {
        innerJoin: () =&amp;gt; createQueryBuilder,
        where: () =&amp;gt; createQueryBuilder,
        skip: () =&amp;gt; createQueryBuilder,
        take: () =&amp;gt; createQueryBuilder,
        getManyAndCount: () =&amp;gt; [tireList, tireList.length],
      };

      tiresRepository.createQueryBuilder.mockImplementation(
        () =&amp;gt; createQueryBuilder,
      );

      const result = await tiresService.findByUserId(userId, page, pageSize);
      expect(result).toMatchObject({
        totalCount: tireList.length,
        data: tireList,
      });
    });&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;회고&lt;/h1&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Fact(사실)&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;지난 프로젝트들과 달리 개인으로 진행하는 프로젝트였다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Feeling(느낀점)&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;개인 프로젝트라 나만 알아보는 코드가 되지 않을까 걱정된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Finding(교훈)&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;최대한 알아보기 쉬운 코드를 작성하도록 해야겠다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Future action(향후 계획)&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;일주일 후 다시 코드를 리뷰하여 리팩토링한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Feedback(피드백)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-&lt;/p&gt;</description>
      <category>원티드</category>
      <category>위코드</category>
      <category>카닥</category>
      <author>haaak</author>
      <guid isPermaLink="true">https://nbkw.tistory.com/204</guid>
      <comments>https://nbkw.tistory.com/204#entry204comment</comments>
      <pubDate>Sat, 27 Nov 2021 17:43:06 +0900</pubDate>
    </item>
    <item>
      <title>[wanted-pre-onboarding] Assignment6</title>
      <link>https://nbkw.tistory.com/203</link>
      <description>&lt;h1&gt;Github Repo&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/wanted-wecode-subjects/deer-subject&quot;&gt;https://github.com/wanted-wecode-subjects/deer-subject&lt;/a&gt;&lt;/p&gt;
&lt;h1&gt;개요&lt;/h1&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;대여/반납 요금 계산 API 구현&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;기간&lt;/h1&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;2021.11.18 - 2021.11.20&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;개발 환경&lt;/h1&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;TypeScript&lt;/li&gt;
&lt;li&gt;NestJS&lt;/li&gt;
&lt;li&gt;MySQL&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;사용 라이브러리&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;typeorm&lt;/li&gt;
&lt;li&gt;class-validator&lt;/li&gt;
&lt;li&gt;class-transformer&lt;/li&gt;
&lt;li&gt;moment-timezone&lt;/li&gt;
&lt;li&gt;passport&lt;/li&gt;
&lt;li&gt;jwt&lt;/li&gt;
&lt;li&gt;bcrypt&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;구현 사항&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요구사항&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;유저 API&lt;/li&gt;
&lt;li&gt;요금 계산 API
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;지역별 기본 요금, 분당 요금 적용&lt;/li&gt;
&lt;li&gt;고장으로 인한 1분 이내 반납시 요금 미청구&lt;/li&gt;
&lt;li&gt;벌금 조건 적용&lt;/li&gt;
&lt;li&gt;할인 적용&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;DB 모델링&lt;/h1&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;484&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b1vl2W/btrlLwjCgIU/WpnKCplclk51cZ5Gfwzkj0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b1vl2W/btrlLwjCgIU/WpnKCplclk51cZ5Gfwzkj0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b1vl2W/btrlLwjCgIU/WpnKCplclk51cZ5Gfwzkj0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb1vl2W%2FbtrlLwjCgIU%2FWpnKCplclk51cZ5Gfwzkj0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;484&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;484&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h1&gt;MySQL spatial data&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL은 OpenGIS 클래스에 해당하는 공간 데이터 타입을 제공합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;926&quot; data-origin-height=&quot;426&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Vn805/btrlDXwGPll/IFMt3XA8f3QvZ80hGLySVk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Vn805/btrlDXwGPll/IFMt3XA8f3QvZ80hGLySVk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Vn805/btrlDXwGPll/IFMt3XA8f3QvZ80hGLySVk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FVn805%2FbtrlDXwGPll%2FIFMt3XA8f3QvZ80hGLySVk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;926&quot; height=&quot;426&quot; data-origin-width=&quot;926&quot; data-origin-height=&quot;426&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;타입&lt;/th&gt;
&lt;th&gt;정의&lt;/th&gt;
&lt;th&gt;예시&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Point&lt;/td&gt;
&lt;td&gt;공간에서 좌표 한 점을 표시&lt;/td&gt;
&lt;td&gt;POINT (10 10)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LineString&lt;/td&gt;
&lt;td&gt;공간에서 다수의 Point를 연결하는 선분을 표시&lt;/td&gt;
&lt;td&gt;LINESTRING (10 10, 20 25, 15 40)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Polygon&lt;/td&gt;
&lt;td&gt;공간에서 다수의 선분이 연결되어 닫힌 상태의 다각형을 표시&lt;/td&gt;
&lt;td&gt;POLYGON ((10 10, 10 20, 20 20, 20 10, 10 10))&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multi-Point&lt;/td&gt;
&lt;td&gt;공간에서 다수의 Point 집합&lt;/td&gt;
&lt;td&gt;MULTIPOINT (10 10, 30 20, 30 10, 10, 20)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multi-LineString&lt;/td&gt;
&lt;td&gt;공간에서 다수의 LineString 집합&lt;/td&gt;
&lt;td&gt;MULTILINESTRING ((10 10, 20 20), (20 15, 30 40))&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multi-Polygon&lt;/td&gt;
&lt;td&gt;공간에서 다수의 Polygon 집합&lt;/td&gt;
&lt;td&gt;MULTIPOLYGON ( (( 10 10, 15 10, 20 15, 20 25, 15 20, 10 10 )) , (( 40 25, 50 40, 35 35, 25 10, 40 25 )) )&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GeomCollection&lt;/td&gt;
&lt;td&gt;모든 공간 데이터들의 집합&lt;/td&gt;
&lt;td&gt;GEOMETRYCOLLECTION ( POINT (10 10), LINESTRING (20 20, 30 40), POINT (30 15) )&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출처: &lt;a href=&quot;https://sparkdia.tistory.com/24&quot;&gt;https://sparkdia.tistory.com/24&lt;/a&gt;&lt;br /&gt;&lt;a href=&quot;https://dev.mysql.com/doc/refman/8.0/en/spatial-type-overview.html&quot;&gt;https://dev.mysql.com/doc/refman/8.0/en/spatial-type-overview.html&lt;/a&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;typeOrm에서의 spatial data&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;typeOrm에서 엔티티의 column에 spatial 타입을 적용하려면 다음과 같이 @Column() 데코레이터에 옵션을 부여합니다.&lt;br /&gt;area.entity.ts&lt;/p&gt;
&lt;pre class=&quot;lasso&quot;&gt;&lt;code&gt;  ...
  @Column({ type: 'polygon', srid: 4326 })
  area_boundary: string;
  ...&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;MySQL spatial data 내장 함수&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;ST_AsText&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공간 데이터 형태의 값을 &lt;a href=&quot;https://mariadb.com/kb/en/wkt-definition/&quot;&gt;WKT&lt;/a&gt; 형식으로 변환하고 문자열로 반환합니다.&lt;/p&gt;
&lt;pre class=&quot;lsl&quot;&gt;&lt;code&gt;SET @g = 'LineString(1 1,4 4,6 6)';

SELECT ST_AsText(ST_GeomFromText(@g));&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과&lt;/p&gt;
&lt;pre class=&quot;asciidoc&quot;&gt;&lt;code&gt;+--------------------------------+
| ST_AsText(ST_GeomFromText(@g)) |
+--------------------------------+
| LINESTRING(1 1,4 4,6 6)        |
+--------------------------------+&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;ST_Contains&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫번째 인자 g1이 두번째 인자 g2에 포함되면 1, 포함되지 않으면 0을 반환합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예제1&lt;/p&gt;
&lt;pre class=&quot;lsl&quot;&gt;&lt;code&gt;SET @g1 = ST_GEOMFROMTEXT('POLYGON((175 150, 20 40, 50 60, 125 100, 175 150))');
SET @g2 = ST_GEOMFROMTEXT('POINT(174 149)');

SELECT ST_CONTAINS(@g1,@g2);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;+----------------------+
| ST_CONTAINS(@g1,@g2) |
+----------------------+
|                    1 |
+----------------------+&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예제2&lt;/p&gt;
&lt;pre class=&quot;lsl&quot;&gt;&lt;code&gt;SET @g1 = ST_GEOMFROMTEXT('POLYGON((175 150, 20 40, 50 60, 125 100, 175 150))');
SET @g2 = ST_GEOMFROMTEXT('POINT(175 151)');

SELECT ST_CONTAINS(@g1,@g2);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;+----------------------+
| ST_CONTAINS(@g1,@g2) |
+----------------------+
|                    0 |
+----------------------+&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;ST_Distance&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인자로 받은 두 공간 데이터의 거리를 반환합니다.&lt;br /&gt;인자값이 유효하지 않으면 null을 반환합니다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;SELECT ST_Distance(POINT(1,2),POINT(2,2));&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과&lt;/p&gt;
&lt;pre class=&quot;asciidoc&quot;&gt;&lt;code&gt;+------------------------------------+
| ST_Distance(POINT(1,2),POINT(2,2)) |
+------------------------------------+
|                                  1 |
+------------------------------------+&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;ST_GeomFromText&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;WKT와 SRID를 사용하여 모든 유형의 공간 데이터 값을 구성합니다.&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;SET @g = ST_GEOMFROMTEXT('POINT(1 1)');
SELECT @g;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과&lt;/p&gt;
&lt;pre class=&quot;asciidoc&quot;&gt;&lt;code&gt;+--------------------------------------------------+
|                    ST_GEOMFROMTEXT('POINT(1 1)') |
+--------------------------------------------------+
| 0x0000000001010000000000000000003F0000000000003F |
+--------------------------------------------------+&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출처: &lt;a href=&quot;https://kibua20.tistory.com/189&quot;&gt;https://kibua20.tistory.com/189&lt;/a&gt;&lt;br /&gt;&lt;a href=&quot;https://mariadb.com/kb/en/st_astext/&quot;&gt;https://mariadb.com/kb/en/st_astext/&lt;/a&gt;&lt;br /&gt;&lt;a href=&quot;https://mariadb.com/kb/en/st-contains/&quot;&gt;https://mariadb.com/kb/en/st-contains/&lt;/a&gt;&lt;br /&gt;&lt;a href=&quot;https://mariadb.com/kb/en/st_distance/&quot;&gt;https://mariadb.com/kb/en/st_distance/&lt;/a&gt;&lt;br /&gt;&lt;a href=&quot;https://mariadb.com/kb/en/st_geomfromtext/&quot;&gt;https://mariadb.com/kb/en/st_geomfromtext/&lt;/a&gt;&lt;/p&gt;
&lt;h1&gt;bcrypt mocking&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;bcrypt를 직접 import 하여 jest.spyOn으로 bcrypt의 함수를 mocking 한 뒤, 테스트에 사용할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;user.service.spec.ts&lt;/p&gt;
&lt;pre class=&quot;lasso&quot;&gt;&lt;code&gt;...
import * as bcrypt from 'bcrypt';
...
      jest.spyOn(bcrypt, 'compare').mockResolvedValue(true);
      ...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고: &lt;a href=&quot;https://stackoverflow.com/questions/62646538/jest-how-to-test-a-function-within-a-method&quot;&gt;https://stackoverflow.com/questions/62646538/jest-how-to-test-a-function-within-a-method&lt;/a&gt;&lt;/p&gt;
&lt;h1&gt;이슈&lt;/h1&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;MySQL Version:AsText() is deprecated as of MySQL 5.7.6&lt;/h2&gt;
&lt;pre class=&quot;subunit&quot;&gt;&lt;code&gt;ERROR [ExceptionsHandler] ER_SP_DOES_NOT_EXIST: FUNCTION deer.AsText does not exist
QueryFailedError: ER_SP_DOES_NOT_EXIST: FUNCTION deer.AsText does not exist
    at QueryFailedError.TypeORMError [as constructor] (/Users/hak/code/deer-subject/src/error/TypeORMError.ts:7:9)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버 실행시 상기 에러가 발생하여 찾아본 결과 AsText() 함수가 deprecate 되어 발생한 에러였습니다.&lt;br /&gt;이를 해결하기 위해 TypeOrmModule 설정에서 &lt;code&gt;legacySpatialSupport: false&lt;/code&gt; 를 추가하여 해결하였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;app.module.ts&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;    ...
    TypeOrmModule.forRoot({
      type: 'mysql',
      ...
      legacySpatialSupport: false,
    }),
    ...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고: &lt;a href=&quot;https://github.com/mysqljs/mysql/issues/2127&quot;&gt;https://github.com/mysqljs/mysql/issues/2127&lt;/a&gt;&lt;/p&gt;
&lt;h1&gt;회고&lt;/h1&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Fact(사실)&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;저번 프로젝트에서 Heroku의 sqlite 데이터가 사라지는 이슈가 있었다. 이는 dyno가 일시적인 파일시스템을 사용하므로 잠자기 상태에 들어가면 파일들이 삭제되는 현상이다.&lt;/li&gt;
&lt;li&gt;오픈 API에 요청을 보낼 때 응답하지 않는 경우 서버는 이를 계속 기다릴 수 있으므로 timeout을 걸어놔야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Feeling(느낀점)&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;dyno의 파일시스템에 알지 못한 상태로 데이터가 사라지는 현상이 발생하여 당황하였다.&lt;/li&gt;
&lt;li&gt;시스템 구축 당시 무한으로 대기하는 경우에 대해 고려하지 못한 것이 아쉬웠다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Finding(교훈)&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서버를 띄우는 환경에 대해 정확히 파악한 후 이에 맞는 환경을 구축해야겠다.&lt;/li&gt;
&lt;li&gt;다양한 상황에 대해 대처할 수 있도록 고려하여 시스템을 구축해야겠다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Future action(향후 계획)&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Heroku에 서버를 띄울 경우 sqlite 대신 다른 DB를 사용&lt;/li&gt;
&lt;li&gt;Heroku에 sqlite로 서버를 띄울 경우 서버가 잠들지 않도록 구현(예: cron, 외부 서비스(&lt;a href=&quot;https://kaffeine.herokuapp.com/)%EB%A5%BC&quot;&gt;https://kaffeine.herokuapp.com/)를&lt;/a&gt; 사용)&lt;/li&gt;
&lt;li&gt;axios 사용시 요청에 대한 timeout을 설정&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Feedback(피드백)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-&lt;/p&gt;</description>
      <category>디어코퍼레이션</category>
      <category>원티드</category>
      <category>위코드</category>
      <author>haaak</author>
      <guid isPermaLink="true">https://nbkw.tistory.com/203</guid>
      <comments>https://nbkw.tistory.com/203#entry203comment</comments>
      <pubDate>Sun, 21 Nov 2021 18:02:41 +0900</pubDate>
    </item>
    <item>
      <title>[wanted-pre-onboarding] Assignment5</title>
      <link>https://nbkw.tistory.com/202</link>
      <description>&lt;h1&gt;Github Repo&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/wanted-wecode-subjects/humanscape-subject&quot;&gt;https://github.com/wanted-wecode-subjects/humanscape-subject&lt;/a&gt;&lt;/p&gt;
&lt;h1&gt;개요&lt;/h1&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;임상실험 데이터 수집 API 구현&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;기간&lt;/h1&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;2021.11.15 - 2021.11.17&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;개발 환경&lt;/h1&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;TypeScript&lt;/li&gt;
&lt;li&gt;NestJS&lt;/li&gt;
&lt;li&gt;Sqlite3&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;사용 라이브러리&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;typeorm&lt;/li&gt;
&lt;li&gt;class-validator&lt;/li&gt;
&lt;li&gt;class-transformer&lt;/li&gt;
&lt;li&gt;moment-timezone&lt;/li&gt;
&lt;li&gt;date-fns&lt;/li&gt;
&lt;li&gt;xml2json-light&lt;/li&gt;
&lt;li&gt;schedule&lt;/li&gt;
&lt;li&gt;axios&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;구현 사항&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요구사항&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;임상정보 수집 batch task&lt;/li&gt;
&lt;li&gt;특정 임상정보 읽기&lt;/li&gt;
&lt;li&gt;최근 일주일내에 업데이트 된 임상정보 리스트 조회
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;pagination&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;DB 모델링&lt;/h1&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;363&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bvyiuP/btrlbDrE1qh/BrHxTd5EKpLD4aeSkozG90/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bvyiuP/btrlbDrE1qh/BrHxTd5EKpLD4aeSkozG90/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bvyiuP/btrlbDrE1qh/BrHxTd5EKpLD4aeSkozG90/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbvyiuP%2FbtrlbDrE1qh%2FBrHxTd5EKpLD4aeSkozG90%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;363&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;363&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h1&gt;임상정보 리스트 조회 API&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://pocky-humanscape-subject.herokuapp.com/clinical&quot;&gt;https://pocky-humanscape-subject.herokuapp.com/clinical&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1276&quot; data-origin-height=&quot;497&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/eses7o/btrlhDqaEJu/v5pOJXzCfbYFBeVRiK8cCK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/eses7o/btrlhDqaEJu/v5pOJXzCfbYFBeVRiK8cCK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/eses7o/btrlhDqaEJu/v5pOJXzCfbYFBeVRiK8cCK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Feses7o%2FbtrlhDqaEJu%2Fv5pOJXzCfbYFBeVRiK8cCK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1276&quot; height=&quot;497&quot; data-origin-width=&quot;1276&quot; data-origin-height=&quot;497&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h1&gt;axios&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;nodejs에는 fetch가 없습니다.&lt;br /&gt;따라서 이에 대한 대책으로 node-fetch 또는 axios 패키지를 사용합니다.&lt;br /&gt;nestjs에서는 @nestjs/axios를 제공합니다.&lt;br /&gt;해당 패키지의 HttpModule을 사용하려는 모듈에 import 하여 HttpService를 사용할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;clinical.module.ts&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@Module({
  imports: [
    TypeOrmModule.forFeature([ClinicalRepository]),
    HttpModule,
    StepModule,
  ],
  controllers: [ClinicalController],
  providers: [ClinicalService],
})
export class ClinicalModule {}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;clinical.service.ts&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;export class ClinicalService {
  constructor(
    @InjectRepository(ClinicalRepository)
    private readonly clinicalRepository: ClinicalRepository,
    private httpService: HttpService,
    private stepService: StepService,
  ) {}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;get&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HttpService.get() 메소드를 사용하여 get 요청을 할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;clinical.service.ts&lt;/p&gt;
&lt;pre class=&quot;qml&quot;&gt;&lt;code&gt;  getAPIData(pageNo, start = 0) {
    let url =
      'http://apis.data.go.kr/1470000/MdcinClincTestInfoService/getMdcinClincTestInfoList';
    url += '?' + `ServiceKey=${process.env.SERVICE_KEY}`;
    url += '&amp;amp;' + `numOfRows=${CLINICAL_CONSTANT.NUM_OF_ROWS}`;
    url += '&amp;amp;' + `pageNo=${pageNo}`;

    return this.httpService.get(url);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러나 axios의 get 메소드는 rxjs를 사용하여 이벤트 발생에 따른 동작을 수행합니다. 그래서 Observable 객체에 래핑된 AxiosResponse를 반환합니다.&lt;br /&gt;반환받은 값을 통해 이후 동작을 정의하는 데에는 다음과 같은 방법이 있습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;반환된 값을 받아서 subscirbe() 메소드를 사용하여 이후 기능을 구현&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;aspectj&quot;&gt;&lt;code&gt;const observer = this.getAPIData(pageNo);
observer.subscribe({
  next(resposne) {
    ... // response 가공 후 DB에 저장
  },
  error(err) {
    ... // error 관련 처리
  },
  complete() {
    ... // 완료 후 처리
  }
});&lt;/code&gt;&lt;/pre&gt;
&lt;ol style=&quot;list-style-type: disc;&quot; start=&quot;2&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;get()의 반환값에 pipe() 메소드를 사용하여 함수형 프로그래밍으로 이후 기능을 구현&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;  ...
  const observer = this.httpService.get(url).pipe(map(axiosResponse) =&amp;gt; axiosResponse);
  const doSomething = () =&amp;gt; {
    const jsonResponse = xml2json.xml2json(axiosResponse.data);
    const items = jsonResponse.response.body.items.item; // 임상 실험 데이터 배열

    if (!items) {
      return;
    }

    return items;
  }
  // api의 임상정보 데이터 배열(items)이 res에 담김
  const res = await lastValueFrom(observer).then(doSomething);
  ... // DB에 clinical 저장&lt;/code&gt;&lt;/pre&gt;
&lt;ol style=&quot;list-style-type: disc;&quot; start=&quot;3&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;get()의 반환값에 toPromise() 메소드를 사용하여 promise 객체로 바꾼뒤, 이후 기능을 구현&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;qml&quot;&gt;&lt;code&gt;  async getAPIData(pageNo, start = 0): Promise&amp;lt;Clinical[]&amp;gt; {
    let url =
      'http://apis.data.go.kr/1470000/MdcinClincTestInfoService/getMdcinClincTestInfoList';
    url += '?' + `ServiceKey=${process.env.SERVICE_KEY}`;
    url += '&amp;amp;' + `numOfRows=${CLINICAL_CONSTANT.NUM_OF_ROWS}`;
    url += '&amp;amp;' + `pageNo=${pageNo}`;

    return this.httpService
      .get(url)
      .toPromise()
      .then(async (axiosResponse) =&amp;gt; {
        const jsonResponse = xml2json.xml2json(axiosResponse.data);
        const items = jsonResponse.response.body.items.item; // 임상 실험 데이터 배열

        if (!items) {
          return;
        }

        // DB에 insert
        for (let i = start; i &amp;lt; items.length; i++) {
          await this.createClinical(items[i]);
        }

        return items;
      });
  }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이외에도 다양한 방법이 있겠지만, 과제 진행 당시에는 조급한 마음에 다른 선택지가 보이지 않고 세번째 방법을 선택하여 코드를 작성하였습니다.&lt;br /&gt;(참고로 세번째 방법의 toPromise()는 deprecate 될 예정입니다)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고: &lt;a href=&quot;https://min9nim.vercel.app/2020-04-24-rxjs/&quot;&gt;https://min9nim.vercel.app/2020-04-24-rxjs/&lt;/a&gt;&lt;br /&gt;&lt;a href=&quot;https://ichi.pro/ko/rxjs-7ui-saeloun-gineung-177144942872256&quot;&gt;https://ichi.pro/ko/rxjs-7ui-saeloun-gineung-177144942872256&lt;/a&gt;&lt;br /&gt;&lt;a href=&quot;https://docs.nestjs.kr/techniques/http-module&quot;&gt;https://docs.nestjs.kr/techniques/http-module&lt;/a&gt;&lt;/p&gt;
&lt;h1&gt;xml2json-light&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고한 openAPI 데이터는 반환형이 XML로 고정되어 있기 때문에 이를 json으로 바꿀 필요가 있었습니다.&lt;br /&gt;때문에 이와 관련된 라이브러리인 xml2json-light를 사용하였습니다.&lt;br /&gt;xml2json-light의 xml2json() 함수를 사용하면 XML 문자열이 JSON 객체로 변환됩니다.&lt;/p&gt;
&lt;pre class=&quot;xquery&quot;&gt;&lt;code&gt;import * as xml2json from 'xml2json-light';

const xml = `&amp;lt;APPLY_ENTP_NAME&amp;gt;한국아스트라제네카&amp;lt;/APPLY_ENTP_NAME&amp;gt;
&amp;lt;APPROVAL_TIME&amp;gt;2012-03-22 00:00:00&amp;lt;/APPROVAL_TIME&amp;gt;
&amp;lt;LAB_NAME&amp;gt;학교법인 고려중앙학원 고려대학교의과대학부속병원:서울대학교병원:분당서울대학교병원:연세대학교의과대학 강남세브란스병원:전북대학교병원:연세대학교의과대학 강남세브란스병원&amp;lt;/LAB_NAME&amp;gt;
&amp;lt;GOODS_NAME&amp;gt;AZD8931&amp;lt;/GOODS_NAME&amp;gt;
&amp;lt;CLINIC_EXAM_TITLE&amp;gt;HER2 상태에 따른 트라스투주맙 치료에 적합하지 않고, 일차요법에서 진행 된 전이성, 위암 또는 위-식도 접합부 암환자에서 파클리탁셀 단독 투여 대비 파클리탁셀과 병용한 AZD8931의 유효성, 안전성 및 약물동력학 평가를 위한 제 2a상 다기관 무작위배정 이중맹검 위약대조 시험 (SAGE)&amp;lt;/CLINIC_EXAM_TITLE&amp;gt;
&amp;lt;CLINIC_STEP_NAME&amp;gt;2a상&amp;lt;/CLINIC_STEP_NAME&amp;gt;`;
const json = xml2json.xml2json(xml); 
console.log(json);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;console&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;{
  APPLY_ENTP_NAME: &amp;lsquo;한국아스트라제네카&amp;rsquo;,
  APPROVAL_TIME: &amp;lsquo;2012-03-22 00:00:00&amp;rsquo;,
  LAB_NAME: &amp;lsquo;학교법인 고려중앙학원 고려대학교의과대학부속병원:서울대학교병원:분당서울대학교병원:연세대학교의과대학 강남세브란스병원:전북대학교병원:연세대학교의과대학 강남세브란스병원&amp;rsquo;,
  GOODS_NAME: &amp;lsquo;AZD8931&amp;rsquo;,
  CLINIC_EXAM_TITLE: &amp;lsquo;HER2 상태에 따른 트라스투주맙 치료에 적합하지 않고, 일차요법에서 진행 된 전이성, 위암 또는 위-식도 접합부 암환자에서 파클리탁셀 단독 투여 대비 파클리탁셀과 병용한 AZD8931의 유효성, 안전성 및 약물동력학 평가를 위한 제 2a상 다기관 무작위배정 이중맹검 위약대조 시험 (SAGE)&amp;rsquo;,
  CLINIC_STEP_NAME: &amp;lsquo;2a상&amp;rsquo;
}&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;batch&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;nestjs에서는 Node.js &lt;a href=&quot;https://www.npmjs.com/package/node-cron&quot;&gt;node-cron&lt;/a&gt; 패키지와 통합되는 @nestjs/schedule 패키지를 제공합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;@cron&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;cron은 linux의 cron 처럼 특정 시간마다 정의한 메소드를 호출할 수 있도록 도와주는 데코레이터입니다.&lt;br /&gt;데코레이터에 전달되는 값은 초 분 시 일 월 요일 순이며, *을 입력하면 매번 실행, 범위를 지정하고 싶다면(예: 1분~30분마다 실행) 1-30 과 같이 작성할 수 있습니다. 또한 /를 사용하여 주기적으로 실행할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예시&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;* * * * * *&lt;/code&gt;: 매 초 실행&lt;/li&gt;
&lt;li&gt;&lt;code&gt;45 * * * * *&lt;/code&gt;: 매분 45초에 실행&lt;/li&gt;
&lt;li&gt;&lt;code&gt;0 10 * * * *&lt;/code&gt;: 매시간 10분 0초에 실행&lt;/li&gt;
&lt;li&gt;&lt;code&gt;0 */30 9-17 * * *&lt;/code&gt;: 오전 9시부터 오후 5시까지 30분마다 실행&lt;/li&gt;
&lt;li&gt;&lt;code&gt;0 30 11 * * 1-5&lt;/code&gt;: 월요일(1)부터 금요일(5) 오전 11시 30분에 실행&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 프로젝트에서는 월~토요일 자정 정각에 batch를 돌릴 수 있도록 cron을 설정하였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;clinical.service.ts&lt;/p&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;  @Cron('0 0 0 * * 1-6')
  async batchData(): Promise&amp;lt;void&amp;gt; {
    ...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고: &lt;a href=&quot;https://docs.nestjs.kr/techniques/task-scheduling&quot;&gt;https://docs.nestjs.kr/techniques/task-scheduling&lt;/a&gt;&lt;/p&gt;
&lt;h1&gt;회고&lt;/h1&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Fact(사실)&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;xml2json이 일부 피씨에서 설치되지 않는 이슈가 있었다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Feeling(느낀점)&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;맥북에어 m1에서도 작동하고 윈도우에서도 작동하는데 맥미니 m1과 일부 윈도우에서 설치가 되지 않는 것이 의아했다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Finding(교훈)&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;npm 패키지도 모든 피씨에서 동일하게 작동하지 않는다는 것을 깨달았다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Future action(향후 계획)&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;xml2json이 아닌 xml2json-light로 패키지를 바꿈&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Feedback(피드백)&lt;/h2&gt;</description>
      <category>원티드</category>
      <category>위코드</category>
      <category>휴먼스케이프</category>
      <author>haaak</author>
      <guid isPermaLink="true">https://nbkw.tistory.com/202</guid>
      <comments>https://nbkw.tistory.com/202#entry202comment</comments>
      <pubDate>Wed, 17 Nov 2021 12:30:00 +0900</pubDate>
    </item>
    <item>
      <title>[wanted-pre-onboarding] Assignment4</title>
      <link>https://nbkw.tistory.com/201</link>
      <description>&lt;h1&gt;Github Repo&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/wanted-wecode-subjects/eight-percent-subject&quot;&gt;https://github.com/wanted-wecode-subjects/eight-percent-subject&lt;/a&gt;&lt;/p&gt;
&lt;h1&gt;개요&lt;/h1&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;회원, 계좌, 입/출금 API 만들기&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;기간&lt;/h1&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;2021.11.11 - 2021.11.13&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;개발 환경&lt;/h1&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;TypeScript&lt;/li&gt;
&lt;li&gt;NestJS&lt;/li&gt;
&lt;li&gt;Sqlite3&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;사용 라이브러리&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;bcrypt&lt;/li&gt;
&lt;li&gt;passport&lt;/li&gt;
&lt;li&gt;typeorm&lt;/li&gt;
&lt;li&gt;class-validator&lt;/li&gt;
&lt;li&gt;class-transformer&lt;/li&gt;
&lt;li&gt;passport-jwt&lt;/li&gt;
&lt;li&gt;moment&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;구현 사항&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요구사항&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;sqlite 사용&lt;/li&gt;
&lt;li&gt;거래내역 조회
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;계좌의 소유주만 요청 가능&lt;/li&gt;
&lt;li&gt;거래일시 필터링&lt;/li&gt;
&lt;li&gt;출금/입금 필터링&lt;/li&gt;
&lt;li&gt;페이지네이션&lt;/li&gt;
&lt;li&gt;거래일시, 거래금액, 잔액, 거래종류(출금/입금), 적요&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;입금
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;계좌의 소유주만 요청 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;출금
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;계좌의 소유주만 요청 가능&lt;/li&gt;
&lt;li&gt;계좌 잔액 내에서만 출금 가능&lt;/li&gt;
&lt;li&gt;잔액을 넘긴 출금 요청에 적절한 에러처리&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;기타
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;유닛 테스트 구현&lt;/li&gt;
&lt;li&gt;functional test 구현&lt;/li&gt;
&lt;li&gt;거래내역이 1억건 넘어갈 때를 고려
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;1억건이 넘는 조회에 대한 설계 작성&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;DB 모델링&lt;/h1&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;618&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cMjtwO/btrkOkK2avA/QUmL1IlxMqhAX1jRfAtA1k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cMjtwO/btrkOkK2avA/QUmL1IlxMqhAX1jRfAtA1k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cMjtwO/btrkOkK2avA/QUmL1IlxMqhAX1jRfAtA1k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcMjtwO%2FbtrkOkK2avA%2FQUmL1IlxMqhAX1jRfAtA1k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;618&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;618&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h1&gt;typeOrm foreign key가 안 들어올 때&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@GetUser 데코레이터로 user에 대한 정보를 받아서 account 서비스에 넘긴 뒤 account를 생성하였는데 실제 테이블에 들어가서 확인해보니 NULL값으로 되어 있었습니다.&lt;br /&gt;즉, foreign key가 제대로 입력되지 않았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 jwtStrategy로 가서 user에 대한 정보를 모두 리턴하였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;jwt.strategy.ts&lt;/p&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;  async validate(payload: any) {
    const { user_id, loginedAt } = payload;
    const user = await this.usersService.findOneByUserId(user_id);

    const tokenLoginedAt = new Date(loginedAt).getTime();
    const userLoginedAt = new Date(user.loginedAt).getTime();

    if (tokenLoginedAt !== userLoginedAt) {
      throw new UnauthorizedException('올바르지 않은 토큰입니다');
    }
    return { ...user };
  }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 테이블로 가서 확인해보니 해당 데이터가 입력되었습니다.&lt;br /&gt;하지만 쓸데없는 정보나 보안상 이슈가 될 수 있는 비밀번호 같은 데이터가 들어가 논란이 생길 수 있었습니다.&lt;br /&gt;그래서 기존에 줘야하던 정보(user_id, loginedAt) 이외에 user의 id 값을 넘겨보았습니다.&lt;/p&gt;
&lt;pre class=&quot;qml&quot;&gt;&lt;code&gt;  async validate(payload: any) {
    const { user_id, loginedAt } = payload;
    const user = await this.usersService.findOneByUserId(user_id);

    const tokenLoginedAt = new Date(loginedAt).getTime();
    const userLoginedAt = new Date(user.loginedAt).getTime();

    if (tokenLoginedAt !== userLoginedAt) {
      throw new UnauthorizedException('올바르지 않은 토큰입니다');
    }
    return { id: user.id, user_id, loginedAt };
  }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 account를 생성한 뒤 테이블을 확인한 결과, 해당 row에 foreign key가 입력이 된 것을 확인할 수 있었습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1002&quot; data-origin-height=&quot;618&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bi1DUt/btrkIQYv9u1/QickMF81s2Waqwq7ExKljk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bi1DUt/btrkIQYv9u1/QickMF81s2Waqwq7ExKljk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bi1DUt/btrkIQYv9u1/QickMF81s2Waqwq7ExKljk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbi1DUt%2FbtrkIQYv9u1%2FQickMF81s2Waqwq7ExKljk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;581&quot; height=&quot;358&quot; data-origin-width=&quot;1002&quot; data-origin-height=&quot;618&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h1&gt;회고&lt;/h1&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Fact(사실)&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;저번 프로젝트에서 비즈니스 로직을 컨트롤러 단에서 처리하였다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Feeling(느낀점)&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;해당 기능과 관련있는 커스텀 repository를 생성하지 않아 헷갈려서 비즈니스 로직을 컨트롤러 단에서 처리한 점이 아쉬웠다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Finding(교훈)&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;커스텀 repository를 정의하여 위와 같은 이슈가 발생하지 않도록 해야 겠다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Future action(향후 계획)&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;비즈니스 로직은 서비스 단에서 처리한다.&lt;/li&gt;
&lt;li&gt;트랜잭션을 사용하는 기능은 서비스 단에서 처리한다.&lt;/li&gt;
&lt;li&gt;도메인별 커스텀 repository를 정의한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Feedback(피드백)&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;컨트롤러에서는 외부 요청과 응답에 대한 부분을 담당한다.&lt;/li&gt;
&lt;li&gt;서비스에서는 비즈니스 로직을 처리한다.&lt;/li&gt;
&lt;li&gt;repository에서는 DB를 통해 데이터를 가져오거나 처리한다.&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>8퍼센트</category>
      <category>원티드</category>
      <category>위코드</category>
      <author>haaak</author>
      <guid isPermaLink="true">https://nbkw.tistory.com/201</guid>
      <comments>https://nbkw.tistory.com/201#entry201comment</comments>
      <pubDate>Sat, 13 Nov 2021 15:18:31 +0900</pubDate>
    </item>
    <item>
      <title>[wanted-pre-onboarding] Assignment3</title>
      <link>https://nbkw.tistory.com/200</link>
      <description>&lt;h1&gt;Github Repo&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/wanted-wecode-subjects/redbrick-subject&quot;&gt;https://github.com/wanted-wecode-subjects/redbrick-subject&lt;/a&gt;&lt;/p&gt;
&lt;h1&gt;개요&lt;/h1&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;회원, 프로젝트, 게임 API 만들기&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;기간&lt;/h1&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;2021.11.08 - 2021.11.10&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;개발 환경&lt;/h1&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;TypeScript&lt;/li&gt;
&lt;li&gt;NestJS&lt;/li&gt;
&lt;li&gt;Sqlite3&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;사용 라이브러리&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;bcrypt&lt;/li&gt;
&lt;li&gt;passport&lt;/li&gt;
&lt;li&gt;typeorm&lt;/li&gt;
&lt;li&gt;class-validator&lt;/li&gt;
&lt;li&gt;passport-jwt&lt;/li&gt;
&lt;li&gt;cache-manager&lt;/li&gt;
&lt;li&gt;schedule&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;구현 사항&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요구사항&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;회원 기능&lt;/li&gt;
&lt;li&gt;게임
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;게임 또는 사용자로 게임 검색&lt;/li&gt;
&lt;li&gt;조회수, 좋아요 기능 구현&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;프로젝트(제작 중인 게임을 의미)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;프로젝트의 실시간 반영&lt;br /&gt;(예를 들어, 프로젝트 수정 중 비정상적으로 사이트가 종료되어도 작업 내용이 보존되어야 함)&lt;/li&gt;
&lt;li&gt;프로젝트 당 퍼블리싱 할 수 있는 게임은 하나&lt;/li&gt;
&lt;li&gt;퍼블리싱 이후 프로젝트를 수정하여 재출시하면 기존에 퍼블리싱된 게임도 수정됨&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;DB 모델링&lt;/h1&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;618&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/VBoqn/btrkn9xIew6/1MLVZhXHko7DAVp11CuCEK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/VBoqn/btrkn9xIew6/1MLVZhXHko7DAVp11CuCEK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/VBoqn/btrkn9xIew6/1MLVZhXHko7DAVp11CuCEK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FVBoqn%2Fbtrkn9xIew6%2F1MLVZhXHko7DAVp11CuCEK%2Fimg.png&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;618&quot; data-ke-mobilestyle=&quot;widthOrigin&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h1&gt;delete vs softDelete&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;delete와 softDelete는 TypeORM의 메소드이며, 데이터를 삭제할 때 사용합니다.&lt;br /&gt;다만, delete는 테이블의 데이터를 실제로 삭제하는 반면, softDelete는 엔티티에 @DeleteDateColumn() 데코레이터가 적용된 컬럼의 값만 수정하고 실제로 데이터를 삭제하지 않습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;softDelete() 코드&lt;br /&gt;projects.service.ts&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;qml&quot;&gt;&lt;code&gt;async delete(id: number): Promise&amp;lt;Project&amp;gt; {
  const project = await this.findOne(id);
  await this.projectRepository.softDelete({ id });
  return project;
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;softDelete()를 적용한 테이블&lt;br /&gt;데이터가 DB에서 지워지지 않고 deletedAt 컬럼이 갱신된 것을 확인할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/GBrMe/btrkr3XB6NS/lKXcVEp4cEa17RB9ePCVT1/img.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h1&gt;typeOrm relations&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테이블 간의 1:1, 1:N, N:M 관계에 대응하기 위해, typeOrm에서는 @OneToOne, @OneToMany, @ManyToMany 데코레이터를 통해 relations 기능을 제공하고 있습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;@OneToOne&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1:1 관계에 있는 테이블 간의 매핑에 사용되며, 이번 프로젝트에서는 프로젝트와 게임에 대해 1:1 매핑을 시도하였습니다.&lt;br /&gt;이는 프로젝트마다 하나의 게임만 퍼블리싱 할 수 있기 때문입니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;단방향 @OneToOne을 적용하기 위해 game entity에 project를 매핑합니다.&lt;br /&gt;또한 게임을 생성할 때 Project 객체를 game에 넣어 생성해야 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;game.entity.ts&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;  @OneToOne(() =&amp;gt; Project, { eager: true })
  @JoinColumn()
  project: Project;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고: &lt;a href=&quot;https://typeorm.io/#/one-to-one-relations/&quot;&gt;https://typeorm.io/#/one-to-one-relations/&lt;/a&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;@OneToMany, @ManyToOne&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1:N, N:1의 관계를 지정하기 위해 @OneToMany와 @ManyToOne을 사용할 수 있습니다. 이번 프로젝트에서는 하나의 유저가 다수의 프로젝트 또는 게임을 소유할 수 있기 때문에 user-project, user-game의 관계에 사용하였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;user.entity.ts&lt;/p&gt;
&lt;pre class=&quot;coffeescript&quot;&gt;&lt;code&gt;  @OneToMany((_type) =&amp;gt; Game, (game) =&amp;gt; game.user, {
    eager: false,
    cascade: true,
  })
  games: Game[];&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;game.entity.ts&lt;/p&gt;
&lt;pre class=&quot;coffeescript&quot;&gt;&lt;code&gt;  @ManyToOne((_type) =&amp;gt; User, (user) =&amp;gt; user.games, {
    onDelete: 'CASCADE',
  })
  user: User;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고: &lt;a href=&quot;https://typeorm.io/#/many-to-one-one-to-many-relations/&quot;&gt;https://typeorm.io/#/many-to-one-one-to-many-relations/&lt;/a&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;@ManyToMany&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@ManyToMany를 사용하면 두 테이블을 조인하여 각 PK를 담은 형태의 테이블을 생성하여 N:M 관계를 명시할 수 있습니다.&lt;br /&gt;이번 프로젝트에서는 게임의 좋아요 기능에 대해 유저가 좋아요를 클릭하였는지 여부를 나타내기 위해 @ManyToMany 기능을 사용하였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;game.entity.ts&lt;/p&gt;
&lt;pre class=&quot;coffeescript&quot;&gt;&lt;code&gt;  @ManyToMany((_type) =&amp;gt; User, (users) =&amp;gt; users.likes, { eager: true })
  likes: User[];&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;user.entity.ts&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;  @ManyToMany((_type) =&amp;gt; Game, (game) =&amp;gt; game.likes, {
    cascade: true,
  })
  @JoinTable({ name: 'users_likes' })
  likes: Game[];&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고: &lt;a href=&quot;https://typeorm.io/#/many-to-many-relations/&quot;&gt;https://typeorm.io/#/many-to-many-relations/&lt;/a&gt;&lt;/p&gt;
&lt;h1&gt;typeOrm or 검색&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;where 절의 &lt;code&gt;or&lt;/code&gt; 검색을 사용하기 위해 다음과 같이 find()의 옵션을 줄 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;db.getRepository(MyModel).find({
  where:[
      {name:&quot;john&quot;},
      {lastName: &quot;doe&quot;}
  ]
})&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출처: &lt;a href=&quot;https://stackoverflow.com/questions/53407236/typeorm-or-operator&quot;&gt;https://stackoverflow.com/questions/53407236/typeorm-or-operator&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또는 queryBuilder의 orWhere() 메소드를 사용하여 or 검색을 수행할 수 있습니다. 이번 프로젝트에서는 게임을 검색할 때 유저와 게임 테이블을 조인하고 검색할 수 있도록 queryBuilder의 orWhere()를 사용하였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;game.service.ts&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;    const data = await this.gameRepository
      .createQueryBuilder('game')
      .innerJoin('game.user', 'user')
      .where(`game.title like :keyword`, { keyword: `%${keyword}%` })
      .orWhere(`user.nickname like :keyword`, { keyword: `%${keyword}%` })
      .limit(limit)
      .offset(offset)
      .getMany();&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;forwardRef()&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 프로젝트에서 project와 game이 컨트롤러에서 서로의 서비스 객체를 사용하는 코드가 있어 모듈에서 export와 import를 하던 중, 순환참조 현상이 발생하였습니다.&lt;br /&gt;이를 해결하기 위해 forwardRef() 함수를 사용하였습니다.&lt;br /&gt;&lt;code&gt;forwardRef()&lt;/code&gt;는 '@nestjs/common'에 존재하며, 대상이 되는 두 모듈과 이를 사용하는 클래스의 생성자에서 forwardRef()를 적용해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;game.module.ts&lt;/p&gt;
&lt;pre class=&quot;coffeescript&quot;&gt;&lt;code&gt;@Module({
  imports: [
    TypeOrmModule.forFeature([GameRepository]),
    forwardRef(() =&amp;gt; ProjectsModule),
  ],
  controllers: [GameController],
  providers: [GameService],
  exports: [GameService],
})
export class GameModule {}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;projects.module.ts&lt;/p&gt;
&lt;pre class=&quot;coffeescript&quot;&gt;&lt;code&gt;@Module({
  imports: [
    TypeOrmModule.forFeature([Project]),
    forwardRef(() =&amp;gt; GameModule),
    MemoryCacheModule,
  ],
  controllers: [ProjectsController],
  providers: [ProjectsService],
  exports: [ProjectsService],
})
export class ProjectsModule {}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;game.controller.ts&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;export class GameController {
  constructor(
    private gameService: GameService,
    @Inject(forwardRef(() =&amp;gt; ProjectsService))
    private projectsService: ProjectsService,
  ) {}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;projects.controller.ts&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;export class ProjectsController {
  constructor(
    private readonly projectsService: ProjectsService,
    @Inject(forwardRef(() =&amp;gt; GameService))
    private readonly gameService: GameService,
    private connection: Connection,
  ) {}&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;typeOrm transaction&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하나의 기능 또는 작업 단위에서 DB의 데이터에 영향을 줄 수 있는 쿼리(insert, update, delete)가 두 번 이상 나오는 경우, 쿼리에 문제가 발생하면 이전의 쿼리도 롤백해야 하는 경우가 있습니다.&lt;br /&gt;이를 위해 트랜잭션을 적용하여 작업이 성공할 경우 commit, 실패할 경우 rollback을 하는데 typeOrm에서는 이를 위해 queryRunner를 지원합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;connection&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;queryRunner는 connection에서 가져올 수 있습니다.&lt;br /&gt;typeOrm의 DB connection을 전역으로 사용하려면 app.module에서 다음과 같은 코드로 적용합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;app.module.ts&lt;/p&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;export class AppModule {
  constructor(private connection: Connection) {}
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;transaction 적용&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;transaction을 적용하기 위해 먼저 대상이 되는 클래스에 connection을 가져옵니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;projects.controller.ts&lt;/p&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;export class ProjectsController {
  constructor(
    ...
    private connection: Connection,
  ) {}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 적용할 메소드에서 &lt;code&gt;connection.createQueryRunner()&lt;/code&gt; 코드를 사용하여 queryRunner를 생성합니다.&lt;br /&gt;그리고 &lt;code&gt;queryRunner.connect()&lt;/code&gt;를 통해 연결한 다음, &lt;code&gt;queryRunner.startTransaction()&lt;/code&gt;으로 트랜잭션을 시작합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;projects.controller.ts&lt;/p&gt;
&lt;pre class=&quot;aspectj&quot;&gt;&lt;code&gt;    const queryRunner = this.connection.createQueryRunner();

    await queryRunner.connect();
    await queryRunner.startTransaction();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;트랜잭션이 시작된 후 기능에 필요한 여러 쿼리(insert, update, delete)를 각각 수행합니다. 이때 exception이 발생하면 catch 하여 롤백(&lt;code&gt;queryRunner.rollbackTransaction()&lt;/code&gt;) 할 수 있도록 try-catch 문을 사용합니다.&lt;br /&gt;모든 작업이 정상적으로 수행되었다면 커밋(&lt;code&gt;queryRunner.commitTransaction()&lt;/code&gt;)을 수행합니다.&lt;br /&gt;이후 finally에서 &lt;code&gt;queryRunner.release()&lt;/code&gt; 를 통해 queryRunner의 연결을 해제합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;projects.controller.ts&lt;/p&gt;
&lt;pre class=&quot;coffeescript&quot;&gt;&lt;code&gt;    try {
      ...
      await queryRunner.commitTransaction();
      return { message: 'publish complete' };
    } catch (error) {
      await queryRunner.rollbackTransaction();
      return { message: 'publish fail' };
    } finally {
      await queryRunner.release();
    }&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;transaction mock&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;transaction에 필요한 queryRunner에 대한 mock을 생성하기 위해 다음과 같은 코드로 mocking을 수행할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;projects.controller.spec.ts&lt;/p&gt;
&lt;pre class=&quot;gauss&quot;&gt;&lt;code&gt;  let connection: Connection;

  const qr = {
    manager: {},
  } as QueryRunner;

  class ConnectionMock {
    createQueryRunner(mode?: 'master' | 'slave'): QueryRunner {
      return qr;
    }
  }

  beforeEach(async () =&amp;gt; {
    Object.assign(qr.manager, {
      save: jest.fn(),
    });
    qr.connect = jest.fn();
    qr.release = jest.fn();
    qr.startTransaction = jest.fn();
    qr.commitTransaction = jest.fn();
    qr.rollbackTransaction = jest.fn();

    const module: TestingModule = await Test.createTestingModule({
      controllers: [ProjectsController],
      providers: [
        ProjectsService,
        GameService,
        { provide: Connection, useClass: ConnectionMock },
      ],
    }).compile();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출처: &lt;a href=&quot;https://stackoverflow.com/questions/63664322/how-to-use-jest-spyon-with-nestjs-transaction-code-on-unit-test&quot;&gt;https://stackoverflow.com/questions/63664322/how-to-use-jest-spyon-with-nestjs-transaction-code-on-unit-test&lt;/a&gt;&lt;/p&gt;
&lt;h1&gt;회고&lt;/h1&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Fact(사실)&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;PR 컨벤션을 안 지키는 경우가 종종 있다.&lt;/li&gt;
&lt;li&gt;github의 이슈에 대해 open/close가 제때 되지 않는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Feeling(느낀점)&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;위의 이슈에 대해 말하기가 불편하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Finding(교훈)&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;컨벤션이나 이슈 처리 같은 것은 팀에서 정한 룰이므로 지켜야 하는게 맞고, 이에 대해 팀원에게 말해줘야 하는 것이 맞다고 생각한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Future action(향후 계획)&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;PR 컨벤션 정도는 github에 PR 템플릿을 설정하여 양식에 맞춰 작성할 수 있도록 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Feedback(피드백)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-&lt;/p&gt;</description>
      <category>레드브릭</category>
      <category>원티드</category>
      <category>위즈랩</category>
      <author>haaak</author>
      <guid isPermaLink="true">https://nbkw.tistory.com/200</guid>
      <comments>https://nbkw.tistory.com/200#entry200comment</comments>
      <pubDate>Wed, 10 Nov 2021 13:44:28 +0900</pubDate>
    </item>
    <item>
      <title>[wanted-pre-onboarding] Assignment2</title>
      <link>https://nbkw.tistory.com/199</link>
      <description>&lt;h1&gt;Github Repo&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/wanted-wecode-subjects/freshcode-subject&quot;&gt;https://github.com/wanted-wecode-subjects/freshcode-subject&lt;/a&gt;&lt;/p&gt;
&lt;h1&gt;개요&lt;/h1&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;회원, 상품 API 만들기&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;기간&lt;/h1&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;2021.11.04 - 2021.11.06&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;개발 환경&lt;/h1&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;TypeScript&lt;/li&gt;
&lt;li&gt;NestJS&lt;/li&gt;
&lt;li&gt;Sqlite3&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;사용 라이브러리&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;bcrypt&lt;/li&gt;
&lt;li&gt;passport&lt;/li&gt;
&lt;li&gt;typeorm&lt;/li&gt;
&lt;li&gt;class-validator&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;구현 사항&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요구사항&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;회원가입&lt;/li&gt;
&lt;li&gt;로그인 JWT 사용&lt;/li&gt;
&lt;li&gt;Request시 Header에 Authorization 체크
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;토큰 없으면 에러 처리&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;상품 CRUD
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;상품 추가/수정/삭제는 관리자(admin 권한)만 가능&lt;/li&gt;
&lt;li&gt;사용자(user 권한)는 상품 조회만 가능&lt;/li&gt;
&lt;li&gt;상품 하나 조회&lt;/li&gt;
&lt;li&gt;상품 전체 조회
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;페이지네이션(페이지당 5개)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;에러 핸들링&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;ERD&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ERD(Entity-Relationship Modelling)는 데이터 모델링 분야에서 사용하는 용어로 개체-관계 모델이라 하며,구조화된 데이터를 도식화 한 표현입니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;ERD cloud&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.erdcloud.com/&quot;&gt;ERD cloud&lt;/a&gt;는 DB 모델링 도구로 ERD 작성을 도와줍니다.&lt;br /&gt;이번 프로젝트에서 팀원의 소개로 처음 사용해보았습니다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.velog.io/images/asyh1047/post/b834a7e1-82a6-4882-a05b-6025321f1692/image.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h1&gt;커밋 메시지 컨벤션&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;커밋 메시지에 대한 형식을 두어 깔끔하게 작성하거나 다른 사람들이 읽기 쉽게끔 컨벤션을 둘 수 있습니다.&lt;br /&gt;팀원과 상의하여 컨벤션을 정의할 수 있으며, 프로젝트 초기에 이를 작성하는 것이 좋습니다.&lt;br /&gt;보통 일반적으로 첫째 줄에 제목을 적고, 둘째 줄부터 본문으로 취급하여 커밋에 대한 내용을 작성합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;예시&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;title convention
&lt;ol style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;feat&lt;/code&gt; - 신규 기능 추가&lt;/li&gt;
&lt;li&gt;&lt;code&gt;fix&lt;/code&gt; - 버그 수정&lt;/li&gt;
&lt;li&gt;&lt;code&gt;docs&lt;/code&gt; - 문서 수정&lt;/li&gt;
&lt;li&gt;&lt;code&gt;style&lt;/code&gt; - 코딩 스타일 관련(로직 변경x)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;refactor&lt;/code&gt; - 코드 리팩터링&lt;/li&gt;
&lt;li&gt;&lt;code&gt;test&lt;/code&gt; - 테스트 코드 관련&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ci&lt;/code&gt; - CI/CD 관련&lt;/li&gt;
&lt;li&gt;&lt;code&gt;chore&lt;/code&gt; - 기타&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;rules
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;제목 끝에 마침표를 적지 않는다.&lt;/li&gt;
&lt;li&gt;제목은 명령문으로 사용한다.&lt;/li&gt;
&lt;li&gt;제목은 과거형을 사용하지 않는다.&lt;/li&gt;
&lt;li&gt;'어떻게' 보다는 '무엇'과 '왜'를 설명한다.&lt;/li&gt;
&lt;li&gt;제목은 50자 이내로 작성한다.&lt;/li&gt;
&lt;li&gt;본문의 각 줄은 72자 이내로 작성한다.&lt;/li&gt;
&lt;li&gt;마지막 줄에는 이슈 id를 작성한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Resolves: 해당 이슈 id&lt;/li&gt;
&lt;li&gt;See also: 관련된 이슈 id&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;클래스 확장&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;너무나 당연한 얘기지만 상속을 통해 클래스를 확장할 수 있습니다.&lt;br /&gt;이번 프로젝트에서는 CoreEntity 라는 클래스를 두어 엔티티가 공통적으로 가지고 있는 createdAt, updatedAt, deletedAt 같은 컬럼을 갖도록 하고, 다른 엔티티 클래스에서 이를 상속하여 사용할 수 있도록 처리하였습니다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.velog.io/images/asyh1047/post/74ab8aa9-b965-4a1a-8a17-8956d908a63e/image.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h1&gt;409 Conflict&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ID 중복에 대한 상태코드를 찾던 중 다음 글(&lt;a href=&quot;https://deveric.tistory.com/62&quot;&gt;https://deveric.tistory.com/62&lt;/a&gt;) 찾게 되었습니다.&lt;br /&gt;해당 글에서는 ID 중복에 대해 상태코드로 423, 403, 409 중 하나를 반환하려 하였으며, 그 중 409를 ID 중복에 대한 상태코드로 반환하게 되었습니다.&lt;br /&gt;위의 각 상태코드에 대한 설명은 다음과 같습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;423(Locked): 메소드의 잠금을 의미&lt;/li&gt;
&lt;li&gt;403(Forbidden): 인가(authorization) 실패 상태 코드&lt;/li&gt;
&lt;li&gt;409(Conflict): 리소스와 충돌이 발생하여 사용자가 이를 반영할 수 있도록 유도&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이로 미루어 볼 때, 423은 가입 이후 해당 리소스(ID)에 대한 잠금과는 무관하여 제외되어야 하고, 403은 ID 중복보다는 데이터 유효성이나 인가 실패에 사용하므로 제외되어야 합니다. 마지막으로 409는 리소스에 대한 충돌이므로 ID라는 PK 자원을 점유한 것에 대한 충돌이기 때문에 적합하다고 볼 수 있습니다.&lt;/p&gt;
&lt;h1&gt;@nestjs/config&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@nestjs/config를 사용하여 환경변수를 가져올 수 있습니다.&lt;br /&gt;외부에서 정의된 환경변수는 전역으로 선언된 process.env를 통해 확인할 수 있습니다. 또한 .env 파일에 정의된 key-value 값을 가져와서 사용할 수도 있습니다. .env 파일 안의 데이터 형태는 &lt;code&gt;key=value&lt;/code&gt; 형태로 정의되어야 합니다.&lt;br /&gt;이를 위해선 @nestjs/config 패키지를 설치하고, AppModule에서 ConfigModule을 import 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;app.module.ts&lt;/p&gt;
&lt;pre class=&quot;coffeescript&quot;&gt;&lt;code&gt;import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';

@Module({
  imports: [ConfigModule.forRoot()],
})
export class AppModule {}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;envFilePath&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ConfigModule에 envFilePath를 두어 .env 파일의 경로를 직접 정의할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;ConfigModule.forRoot({
  envFilePath: ['.env.development.local', '.env.development'],
});&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;custom configuration files&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;custom configuration file은 config 객체를 반환하는 팩토리 함수를 내보냅니다.&lt;br /&gt;환경에 따라 적절한 변수 설정이 필요할 때 사용할 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;예시&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;config/configuration.ts&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;export default () =&amp;gt; ({
  port: parseInt(process.env.PORT, 10) || 3000,
  database: {
    host: process.env.DATABASE_HOST,
    port: parseInt(process.env.DATABASE_PORT, 10) || 5432
  }
});&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;app.module.ts&lt;/p&gt;
&lt;pre class=&quot;coffeescript&quot;&gt;&lt;code&gt;import configuration from './config/configuration';

@Module({
  imports: [
    ConfigModule.forRoot({
      load: [configuration],
    }),
  ],
})
export class AppModule {}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출처: &lt;a href=&quot;https://docs.nestjs.kr/techniques/configuration&quot;&gt;https://docs.nestjs.kr/techniques/configuration&lt;/a&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;env에서 가져온 값을 JwtModule에서 사용하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적인 방법으로 env에서 값을 가져와 JwtModule의 secret으로 설정하면 에러가 발생합니다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@Module({
    imports: [
       JwtModule.register({
          secretOrPrivateKey: process.env.SECRET
       })
    ]
})&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 JwtModule이 인스턴스화 될 때 아직 env에서 해당 값을 가져오지 못해 발생하는 이슈이며, 이를 위해 JwtModule의 registerAsync를 사용하여 해결할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;JwtModule.registerAsync({
    imports: [ConfigModule],
    useFactory: async (configService: ConfigService) =&amp;gt; ({
      secretOrPrivateKey: configService.jwtSecret,
    }),
    inject: [ConfigService],
}),&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출처: &lt;a href=&quot;https://stackoverflow.com/questions/55673424/nestjs-unable-to-read-env-variables-in-module-files-but-able-in-service-files&quot;&gt;https://stackoverflow.com/questions/55673424/nestjs-unable-to-read-env-variables-in-module-files-but-able-in-service-files&lt;/a&gt;&lt;/p&gt;
&lt;h1&gt;bcrypt mock&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트 코드에서 bcrypt 함수를 사용하는 경우가 있었습니다. 그러나 해당 함수에 대한 mock이 없어 테스트에 실패하였습니다.&lt;br /&gt;이를 해결하기 위해 아래와 같이 코드를 작성하면 bcrypt.compare에 대한 mock을 생성할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;rust&quot;&gt;&lt;code&gt;const bcryptCompare = jest.fn().mockResolvedValue(true);
(bcrypt.compare as jest.Mock) = bcryptCompare;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출처: const bcryptCompare = jest.fn().mockResolvedValue(true);&lt;br /&gt;(bcrypt.compare as jest.Mock) = bcryptCompare;&lt;/p&gt;
&lt;h1&gt;role guard&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Role에 대한 데코레이터를 생성하여 보호하고자 하는 메소드에 데코레이터를 적용하였으나, user가 빈 값으로 출력되는 등의 정상적으로 동작하지 않는 이슈가 있었습니다.&lt;br /&gt;이는 @UseGuards(JwtAuthGuard, RoleGuard) 같이 가드를 사용하지 않아서 발생한 이슈이며, 해당 데코레이터를 적용한 결과 &lt;code&gt;context.switchToHttp().getRequest()&lt;/code&gt;의 user 값을 잘 가져오는 것을 확인하였습니다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@UseGuards(JwtAuthGuard, RolesGuard)
@Role(UserRole.admin)
...&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;@Injectable()
export class RolesGuard implements CanActivate {
  constructor(private reflector: Reflector) {}

  canActivate(context: ExecutionContext): boolean {
    const requiredRoles = this.reflector.getAllAndOverride&amp;lt;Role[]&amp;gt;(ROLES_KEY, [
      context.getHandler(),
      context.getClass(),
    ]);
    if (!requiredRoles) {
      return true;
    }
    const { user } = context.switchToHttp().getRequest();
    return requiredRoles.some((role) =&amp;gt; user.roles?.includes(role));
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출처: &lt;a href=&quot;https://docs.nestjs.kr/security/authorization&quot;&gt;https://docs.nestjs.kr/security/authorization&lt;/a&gt;&lt;/p&gt;
&lt;h1&gt;회고&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 회고는 5F(Fact, Feeling, Feedback, Finding, Future action)에 의거하여 작성해보았습니다.&lt;br /&gt;(출처: &lt;a href=&quot;https://codechasseur.tistory.com/102&quot;&gt;https://codechasseur.tistory.com/102&lt;/a&gt;)&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Fact&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;JwtAuthGuard를 구현하였으나 필요한 코드에서 적용되지 않음&lt;/li&gt;
&lt;li&gt;RoleGuard 데코레이터 적용 실패로 인해 데코레이터를 쓰지 않는 방법으로 구현&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Feeling&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기능 구현 이후에 팀원들에게 제대로 전파되지 않았고, 문서화도 제대로 되지 않아 이러한 상황이 발생하였기에 많이 아쉬웠다.&lt;/li&gt;
&lt;li&gt;비효율적인 방법으로 기능을 구현하여 아쉬웠다. 그나마 방법을 알게 되어 다음에는 적용할 수 있을 것이기에 다행이라고 생각한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Finding&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기능 구현을 할 때마다 미팅을 주최하여 팀원들에게 전파하기에는 시간이 낭비되고 비효율적인 방법으로 보인다. 앞서 말한 것과 같이 문서화를 잘해서 구현한 기능을 잘 사용하도록 유도하는 것이 중요하다고 생각한다.&lt;/li&gt;
&lt;li&gt;검색 능력이 중요하다고 생각한다. 키워드를 잘 조합해서 원하는 데이터를 찾을 수 있도록 해야겠다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Future action&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;PR을 올릴 때 문서화를 자세히 한다.&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>원티드</category>
      <category>위코드</category>
      <category>프레시코드</category>
      <author>haaak</author>
      <guid isPermaLink="true">https://nbkw.tistory.com/199</guid>
      <comments>https://nbkw.tistory.com/199#entry199comment</comments>
      <pubDate>Sat, 6 Nov 2021 13:12:42 +0900</pubDate>
    </item>
    <item>
      <title>[wanted-pre-onboarding] Assignment1</title>
      <link>https://nbkw.tistory.com/198</link>
      <description>&lt;h1&gt;Github Repo&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/wanted-wecode-subjects/aimmo-subject-teamB&quot;&gt;https://github.com/wanted-wecode-subjects/aimmo-subject-teamB&lt;/a&gt;&lt;/p&gt;
&lt;h1&gt;개요&lt;/h1&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;회원, 게시글, 댓글 API 만들기&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;개발 기간&lt;/h1&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;2021.11.01 - 2011.11.03&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;개발 환경&lt;/h1&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;TypeScript&lt;/li&gt;
&lt;li&gt;NestJS&lt;/li&gt;
&lt;li&gt;MongoDB&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;사용 라이브러리&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;bcrypt&lt;/li&gt;
&lt;li&gt;passport&lt;/li&gt;
&lt;li&gt;mongoose&lt;/li&gt;
&lt;li&gt;class-validator&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;구현 사항&lt;/h1&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;회원 로그인 구현&lt;/li&gt;
&lt;li&gt;게시글 CRUD&lt;/li&gt;
&lt;li&gt;게시글 pagination&lt;/li&gt;
&lt;li&gt;게시글 조회수 카운트&lt;/li&gt;
&lt;li&gt;게시글 검색&lt;/li&gt;
&lt;li&gt;게시글 카테고리&lt;/li&gt;
&lt;li&gt;댓글 CRUD&lt;/li&gt;
&lt;li&gt;대댓글(depth: 1)&lt;/li&gt;
&lt;li&gt;대댓글 pagination&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;대댓글&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MongoDB self ref을 사용하여 대댓글을 구현하였습니다.&lt;br /&gt;댓글의 schema는 다음과 같습니다.&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;Comment {
  content: string;
  author: string;
  depth: number;
  parentCommentId: string;
  comments: Comment[];
  created_at: Date;
  updated_at: Date;
  postId: string;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;댓글의 경우 depth가 0이 되고, parentCommentId에는 빈 값이 들어갑니다.&lt;br /&gt;그리고 대댓글은 depth가 1이 되며, parentCommentId에는 부모 댓글의 _id가 들어갑니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 과정에서 comments의 comment가 자기 참조를 할 수 있도록 @Prop 데코레이터를 &lt;code&gt;@Prop({type: [{type: mongoose.Schema.Types.ObjectId, ref: 'Comment'}]})&lt;/code&gt; 와 같이 적용하였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;src/comment/comment.schema.ts&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;  @Prop({type: [{type: mongoose.Schema.Types.ObjectId, ref: 'Comment'}]})
  comments: Comment[] | null;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;예시&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;댓글이 존재하는 상황에서 대댓글이 등록되면 다음과 같이 조회가 됩니다.&lt;/p&gt;
&lt;pre class=&quot;json&quot;&gt;&lt;code&gt;{
    &quot;_id&quot;: &quot;618176d0bf01ef0f96c163bb&quot;,
    &quot;postId&quot;: &quot;postId&quot;,
    &quot;updated_at&quot;: &quot;2021-11-02T17:35:12.347Z&quot;,
    &quot;created_at&quot;: &quot;2021-11-02T17:35:12.347Z&quot;,
    &quot;comments&quot;: [
        &quot;618176fabf01ef0f96c163c2&quot;,
    ],
    &quot;depth&quot;: 0,
    &quot;author&quot;: &quot;testuser&quot;,
    &quot;content&quot;: &quot;댓글&quot;,
    &quot;__v&quot;: 3
}&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;회고&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;NoSQL을 처음 써보게 되어 초기에 진행이 더뎠습니다.&lt;br /&gt;또한, 초기에 ORM으로 사용하려던 TypeORM이 MongoDB와는 호환이 잘 안되는 이슈가 있어 진행에 어려움을 겪었습니다.&lt;br /&gt;예를 들어, 데이터 조회시 &lt;code&gt;typeorm mongodb cannot read property 'prototype' of undefined&lt;/code&gt; 같은 에러가 발생하는 경우가 있습니다. 이는 버전에 대한 문제로 인해 발생하는 이슈이며, 이 때문에 mongoose를 대체로 사용하였습니다.&lt;br /&gt;(관련 글: &lt;a href=&quot;https://medium.com/crocusenergy/typeorm-mongodb-%EC%A0%81%EC%9A%A9%EA%B3%BC-%EC%93%B0%EC%A7%80-%EC%95%8A%EA%B2%8C%EB%90%9C-%EC%9D%B4%EC%9C%A0-d3ce790b1455&quot;&gt;TypeORM + MongoDB 적용 및 사용하지 않게 된 이유&lt;/a&gt;)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;NoSQL로 스키마를 작성하고, 기능을 구현하였으나, NoSQL스럽게 스키마를 작성하고 코드를 구현하였는지도 약간 의문스럽습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어, comment의 postId 같은 필드가 foreign key 처럼 사용된 것 같아 꺼림칙합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한가지 다행이라고 생각되는 점은 이후에 NoSQL을 사용할 때 오늘만큼을 헤매지 않을 것 같다는 점입니다.&lt;br /&gt;추후 제가 만들고자 하는 애플리케이션의 DB로 NoSQL을 사용한다거나 관련된 내용을 마주했을 때 좀 더 가벼운 마음으로 개발을 진행할 수 있을 것 같습니다.&lt;/p&gt;</description>
      <category>에이모</category>
      <category>원티드</category>
      <category>위코드</category>
      <author>haaak</author>
      <guid isPermaLink="true">https://nbkw.tistory.com/198</guid>
      <comments>https://nbkw.tistory.com/198#entry198comment</comments>
      <pubDate>Wed, 3 Nov 2021 16:22:59 +0900</pubDate>
    </item>
    <item>
      <title>3. 파일 업로드 &amp;amp; 다운로드 - BE (3)</title>
      <link>https://nbkw.tistory.com/165</link>
      <description>&lt;h2&gt;&lt;span&gt;3) &lt;/span&gt;파일 다운로드 구현하기&lt;/h2&gt;
&lt;h3&gt;실습&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;파일이 업로드되면 파일 정보는 데이터베이스 등에 저장을 하도록 합니다. 그리고 나중에 다운로드 할 때는 저장된 파일의 정보를 찾을 수 있는&lt;span&gt; id &lt;/span&gt;등을 Controller에서 받게 한 뒤 해당 정보를 이용하여 데이터베이스에서 파일 정보를 읽어들이도록 해야 합니다&lt;span&gt;.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;읽어들인 파일 정보&lt;span&gt; response&lt;/span&gt;의&lt;span&gt; setHeader() &lt;/span&gt;메서드를 이용해서 파일명&lt;span&gt;, &lt;/span&gt;파일 타입&lt;span&gt;, &lt;/span&gt;파일의 길이 이런 정보들을 지정을 하고 브라우저가 캐시를 읽지 못하도록&lt;span&gt; no-cache &lt;/span&gt;로 설정한 뒤 헤더 정보로 전송을 하게 합니다&lt;span&gt;. &lt;/span&gt;그리고 이&lt;span&gt; response&lt;/span&gt;로부터&lt;span&gt; HttpServlet Response&lt;/span&gt;에&lt;span&gt; OutputStream&lt;/span&gt;을 이용해서 읽어들인 내용의 파일을 출력하게 하는 겁니다&lt;span&gt;.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;아래 예제에서는 경로를 직접 지정하였기에 실제 환경에 구성할 경우 업로드 할 때 &lt;span&gt;DB&lt;/span&gt;에 &lt;span&gt;insert&lt;/span&gt;했던 파일 정보를 &lt;span&gt;DB&lt;/span&gt;에서 읽어와서 변수에 넣어 설정해야 합니다&lt;span&gt;.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;제공하는&lt;span&gt; connect.png &lt;/span&gt;파일을 윈도우 사용자의 경우&lt;span&gt; c:/tmp/ &lt;/span&gt;디렉토리에 복사하고 맥 사용자의 경우는&lt;span&gt; /tmp &lt;/span&gt;디렉토리에 복사합니다&lt;span&gt;.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;FileController&lt;/span&gt;에&lt;span&gt; download&lt;/span&gt;메소드를 추가합니다&lt;span&gt;.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;response&lt;/span&gt;에&lt;span&gt; header&lt;/span&gt;를 설정합니다&lt;span&gt;.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;파일을&lt;span&gt; outputStream&lt;/span&gt;으로 출력합니다&lt;span&gt;.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;http://localhost:8080/guestbook/download&lt;/span&gt;를 브라우저에서 입력하면 파일이 다운되는 것을 확인할 수 있습니다&lt;span&gt;.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;span&gt;FileController.java&lt;/span&gt;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td width=&quot;566&quot;&gt;
&lt;p&gt;&lt;span&gt;@GetMapping(&quot;/download&quot;)&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/span&gt;public void download(HttpServletResponse response) {&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/span&gt;// &lt;/span&gt;직접 파일 정보를 변수에 저장해 놨지만&lt;span&gt;, &lt;/span&gt;이 부분이&lt;span&gt; db&lt;/span&gt;에서 읽어왔다고 가정한다&lt;span&gt;.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/span&gt;String fileName = &quot;connect.png&quot;;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/span&gt;String saveFileName = &quot;c:/tmp/connect.png&quot;; // &lt;/span&gt;맥일 경우&lt;span&gt; &quot;/tmp/connect.png&quot; &lt;/span&gt;로 수정&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/span&gt;String contentType = &quot;image/png&quot;;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/span&gt;int fileLength = 1116303;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/span&gt;response.setHeader(&quot;Content-Disposition&quot;, &quot;attachment; filename=\&quot;&quot; + fileName + &quot;\&quot;;&quot;);&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/span&gt;response.setHeader(&quot;Content-Transfer-Encoding&quot;, &quot;binary&quot;);&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/span&gt;response.setHeader(&quot;Content-Type&quot;, contentType);&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/span&gt;response.setHeader(&quot;Content-Length&quot;, &quot;&quot; + fileLength);&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/span&gt;response.setHeader(&quot;Pragma&quot;, &quot;no-cache;&quot;);&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/span&gt;response.setHeader(&quot;Expires&quot;, &quot;-1;&quot;);&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/span&gt;try(&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/span&gt;FileInputStream fis = new FileInputStream(saveFileName);&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/span&gt;OutputStream out = response.getOutputStream();&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/span&gt;){&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;/span&gt;int readCount = 0;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;/span&gt;byte[] buffer = new byte[1024];&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/span&gt;while((readCount = fis.read(buffer)) != -1){&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/span&gt;&lt;span&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/span&gt;out.write(buffer,0,readCount);&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/span&gt;}&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/span&gt;}catch(Exception ex){&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/span&gt;throw new RuntimeException(&quot;file Save Error&quot;);&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/span&gt;}&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/span&gt;}&lt;/span&gt;&lt;/p&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;h3&gt;생각해보기&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;파일 다운로드를 구현할 때는 보안에 상당히 신경을 싸야 합니다&lt;span&gt;. &lt;/span&gt;인자로 파일명을 받아들여&lt;span&gt;, &lt;/span&gt;그 파일명을 다운로드 받게 할 경우 해커들은 파일명을&lt;span&gt; &quot;../../etc/passwd&quot; &lt;/span&gt;와 같은 형태로 전달하여 해킹을 시도하게 하는 경우도 있습니다&lt;span&gt;.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;참고 자료&lt;/h3&gt;
&lt;p&gt;&lt;b&gt;&lt;span style=&quot;color: #333333;&quot;&gt;[&lt;/span&gt;&lt;/b&gt;&lt;b&gt;&lt;span style=&quot;color: #333333;&quot;&gt;참고링크&lt;span&gt;] Spring MVC 4 File Download Example - WebSystique&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&lt;a href=&quot;http://websystique.com/springmvc/spring-mvc-4-file-download-example/&quot;&gt;http://websystique.com/springmvc/spring-mvc-4-file-download-example/&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;b&gt;&lt;span style=&quot;color: #333333;&quot;&gt;[&lt;/span&gt;&lt;/b&gt;&lt;b&gt;&lt;span style=&quot;color: #333333;&quot;&gt;참고링크&lt;span&gt;] Spring MVC 4 - File download example&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&lt;a href=&quot;https://www.boraji.com/spring-mvc-4-file-download-example&quot;&gt;https://www.boraji.com/spring-mvc-4-file-download-example&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;</description>
      <category>부스트코스 웹 프로그래밍/6. 웹 앱 개발: 예약서비스 4</category>
      <category>부스트코스</category>
      <category>웹프로그래밍</category>
      <author>haaak</author>
      <guid isPermaLink="true">https://nbkw.tistory.com/165</guid>
      <comments>https://nbkw.tistory.com/165#entry165comment</comments>
      <pubDate>Tue, 13 Aug 2019 15:31:01 +0900</pubDate>
    </item>
    <item>
      <title>3. 파일 업로드 &amp;amp; 다운로드 - BE (2)</title>
      <link>https://nbkw.tistory.com/164</link>
      <description>&lt;h2&gt;&lt;span&gt;2) &lt;/span&gt;파일 업로드 구현하기&lt;/h2&gt;
&lt;h3&gt;실습&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;실습은 이전에 했던 프로젝트인 &lt;span&gt;guestbook&lt;/span&gt;에 이어서 진행합니다&lt;span&gt;.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;maven pom.xml&lt;/span&gt;에 파일 업로드와 관련된 라이브러리를 추가해야 합니다&lt;span&gt;.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;commons-fileupload, commons-io &lt;/span&gt;추가&lt;span&gt;.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td width=&quot;566&quot;&gt;
&lt;p&gt;&lt;span&gt;&amp;lt;dependency&amp;gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/span&gt;&amp;lt;groupId&amp;gt;commons-fileupload&amp;lt;/groupId&amp;gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/span&gt;&amp;lt;artifactId&amp;gt;commons-fileupload&amp;lt;/artifactId&amp;gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/span&gt;&amp;lt;version&amp;gt;1.2.1&amp;lt;/version&amp;gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/span&gt;&amp;lt;/dependency&amp;gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/span&gt;&amp;lt;dependency&amp;gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/span&gt;&amp;lt;groupId&amp;gt;commons-io&amp;lt;/groupId&amp;gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/span&gt;&amp;lt;artifactId&amp;gt;commons-io&amp;lt;/artifactId&amp;gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/span&gt;&amp;lt;version&amp;gt;1.4&amp;lt;/version&amp;gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/span&gt;&amp;lt;/dependency&amp;gt;&lt;/span&gt;&lt;/p&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;DispathcerServlet&lt;/span&gt;에게 멀티파트 요청이 올경우 파일 업로드 처리가 될 수 있도록&lt;span&gt; MultipartResolver&lt;/span&gt;객체를 등록합니다&lt;span&gt;.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;최대&lt;span&gt; 10&lt;/span&gt;메가 크기의 파일이 저장되도록 설정하였습니다&lt;span&gt;.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;DispatcherServlet에&lt;/span&gt;&amp;nbsp;이런 내용들을 알려주기 위해서는 &lt;span&gt;config &lt;/span&gt;안에 있는&lt;span&gt; WebMvcContextConfiguration&lt;/span&gt;에 &lt;span&gt;Bean&lt;/span&gt;을 추가합니다&lt;span&gt;.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;WebMvcContextConfiguration.java&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td width=&quot;566&quot;&gt;
&lt;p&gt;&lt;span&gt;@Bean&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/span&gt;public MultipartResolver multipartResolver() {&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/span&gt;org.springframework.web.multipart.commons.CommonsMultipartResolver multipartResolver = new org.springframework.web.multipart.commons.CommonsMultipartResolver();&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/span&gt;multipartResolver.setMaxUploadSize(10485760); // 1024 * 1024 * 10&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&amp;nbsp; &lt;/span&gt;&lt;span&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;/span&gt;return multipartResolver;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/span&gt;}&lt;/span&gt;&lt;/p&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이제 파일을 업로드 할 수 있는&lt;span&gt; form&lt;/span&gt;을 &lt;span&gt;views&lt;/span&gt;에 작성합니다&lt;span&gt;. form&lt;/span&gt;의 &lt;span&gt;method&lt;/span&gt;와 &lt;span&gt;enctype&lt;/span&gt;을 다음과 같이 지정합니다&lt;span&gt;. &lt;/span&gt;또한 &lt;span&gt;input &lt;/span&gt;태그의 타입을 &lt;span&gt;file&lt;/span&gt;로 지정합니다&lt;span&gt;.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;span&gt;uploadform.jsp&lt;/span&gt;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td width=&quot;566&quot;&gt;
&lt;p&gt;&lt;span&gt;&amp;lt;%@ page language=&quot;java&quot; contentType=&quot;text/html; charset=UTF-8&quot;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/span&gt;pageEncoding=&quot;UTF-8&quot;%&amp;gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&amp;lt;&lt;span&gt;!DOCTYPE html PUBLIC &quot;-//W3C//DTD HTML 4.01 Transitional//EN&quot; &quot;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;http://www.w3.org/TR/html4/loose.dtd&quot;&amp;gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&amp;lt;html&amp;gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&amp;lt;head&amp;gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&amp;lt;meta http-equiv=&quot;Content-Type&quot; content=&quot;text/html; charset=UTF-8&quot;&amp;gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&amp;lt;title&amp;gt;uploadform&amp;lt;/title&amp;gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&amp;lt;/head&amp;gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&amp;lt;body&amp;gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&amp;lt;h1&amp;gt;Upload Form&amp;lt;/h1&amp;gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;lt;form method=&quot;post&quot; action=&quot;upload&quot; enctype=&quot;multipart/form-data&quot;&amp;gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/span&gt;file :&lt;span&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/span&gt;&amp;lt;input type=&quot;file&quot; name=&quot;file&quot;&amp;gt;&amp;lt;br&amp;gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/span&gt;&amp;lt;input type=&quot;submit&quot;&amp;gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;lt;/form&amp;gt;&lt;span&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&amp;lt;/body&amp;gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&amp;lt;/html&amp;gt;&lt;/span&gt;&lt;/p&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;파일 업로드가 성공하면 업로드 성공 메시지를 출력하는&lt;span&gt; uploadok.jsp&lt;/span&gt;파일을 작성합니다&lt;span&gt;.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;span&gt;uploadok.jsp&lt;/span&gt;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td width=&quot;566&quot;&gt;
&lt;p&gt;&lt;span&gt;&amp;lt;%@ page language=&quot;java&quot; contentType=&quot;text/html; charset=UTF-8&quot;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/span&gt;pageEncoding=&quot;UTF-8&quot;%&amp;gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&amp;lt;&lt;span&gt;!DOCTYPE html PUBLIC &quot;-//W3C//DTD HTML 4.01 Transitional//EN&quot; &quot;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;http://www.w3.org/TR/html4/loose.dtd&quot;&amp;gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&amp;lt;html&amp;gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&amp;lt;head&amp;gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&amp;lt;meta http-equiv=&quot;Content-Type&quot; content=&quot;text/html; charset=UTF-8&quot;&amp;gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&amp;lt;title&amp;gt;uploadform&amp;lt;/title&amp;gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&amp;lt;/head&amp;gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&amp;lt;body&amp;gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&amp;lt;h1&amp;gt;&lt;/span&gt;파일 업로드 성공&lt;span&gt;&amp;lt;/h1&amp;gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&amp;lt;/body&amp;gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&amp;lt;/html&amp;gt;&lt;/span&gt;&lt;/p&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;파일 업로드를 처리하는&lt;span&gt; FileController&lt;/span&gt;를 작성합니다&lt;span&gt;.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;Controller &lt;/span&gt;패키지에 &lt;span&gt;FileController.java&lt;/span&gt;를 작성합니다&lt;span&gt;.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;span&gt;FileController.java&lt;/span&gt;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td width=&quot;566&quot;&gt;
&lt;p&gt;&lt;span&gt;package kr.or.connect.guestbook.controller;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;import java.io.FileOutputStream;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;import java.io.InputStream;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;import org.springframework.stereotype.Controller;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;import org.springframework.web.bind.annotation.GetMapping;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;import org.springframework.web.bind.annotation.PostMapping;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;import org.springframework.web.bind.annotation.RequestParam;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;import org.springframework.web.multipart.MultipartFile;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;@Controller&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;public class FileController {&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/span&gt;// get&lt;/span&gt;방식으로 요청이 올 경우 업로드 폼을 보여줍니다&lt;span&gt;.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/span&gt;@GetMapping(&quot;/uploadform&quot;)&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/span&gt;public String uploadform() {&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/span&gt;return &quot;uploadform&quot;;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/span&gt;}&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&amp;nbsp; &amp;nbsp; // &lt;/span&gt;실제 파일이 업로드 요청이 들어올 때의 처리 부분&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/span&gt;@PostMapping(&quot;/upload&quot;)&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/span&gt;public String upload(@RequestParam(&quot;file&quot;) MultipartFile file) {&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/span&gt;// &lt;/span&gt;업로드 파일이 &lt;span&gt;MultipartFile &lt;/span&gt;객체로 들어옴&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/span&gt;System.out.println(&quot;&lt;/span&gt;파일 이름&lt;span&gt; : &quot; + file.getOriginalFilename());&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/span&gt;System.out.println(&quot;&lt;/span&gt;파일 크기&lt;span&gt; : &quot; + file.getSize());&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/span&gt;try(&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/span&gt;// &lt;/span&gt;맥일 경우&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/span&gt;//FileOutputStream fos = new FileOutputStream(&quot;/tmp/&quot; + file.getOriginalFilename());&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/span&gt;// &lt;/span&gt;윈도우일 경우&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/span&gt;FileOutputStream fos = new FileOutputStream(&quot;c:/tmp/&quot; + file.getOriginalFilename());&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/span&gt;InputStream is = file.getInputStream();&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/span&gt;){&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;/span&gt;int readCount = 0;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;/span&gt;byte[] buffer = new byte[1024];&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/span&gt;while((readCount = is.read(buffer)) != -1){&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/span&gt;fos.write(buffer,0,readCount);&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/span&gt;&lt;span&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;/span&gt;}&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/span&gt;}catch(Exception ex){&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/span&gt;throw new RuntimeException(&quot;file Save Error&quot;);&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/span&gt;}&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/span&gt;return &quot;uploadok&quot;;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/span&gt;}&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;}&lt;/span&gt;&lt;/p&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;위와 같은 내용이 작성이 되었다면&lt;span&gt;, &lt;/span&gt;서버를 실행시켜 &lt;span&gt;http://localhost:8080/guestbook/uploadform &lt;/span&gt;을 웹 브라우저로 엽니다&lt;span&gt;.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-filename=&quot;1.png&quot; width=&quot;205&quot; height=&quot;128&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/PlezK/btqxsmqyP8A/aGfBTmssHestuf3WoKsRIk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/PlezK/btqxsmqyP8A/aGfBTmssHestuf3WoKsRIk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/PlezK/btqxsmqyP8A/aGfBTmssHestuf3WoKsRIk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FPlezK%2FbtqxsmqyP8A%2FaGfBTmssHestuf3WoKsRIk%2Fimg.png&quot; data-filename=&quot;1.png&quot; width=&quot;205&quot; height=&quot;128&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;파일을 업로드 합니다. 업&lt;span&gt;로드되면 성공페이지로 이동하는 것을 알 수 있습니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-filename=&quot;2.PNG&quot; width=&quot;378&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/daUhna/btqxsA3bJ1G/k7xrrh0OwEX1wRYkymlol1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/daUhna/btqxsA3bJ1G/k7xrrh0OwEX1wRYkymlol1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/daUhna/btqxsA3bJ1G/k7xrrh0OwEX1wRYkymlol1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdaUhna%2FbtqxsA3bJ1G%2Fk7xrrh0OwEX1wRYkymlol1%2Fimg.png&quot; data-filename=&quot;2.PNG&quot; width=&quot;378&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;c:/tmp &lt;/span&gt;폴더에 가보면 파일이 업로드 된 것을 알 수 있습니다&lt;span&gt;.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-filename=&quot;3.png&quot; width=&quot;197&quot; height=&quot;119&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bBbHAP/btqxqWMUMam/tDakW5Gnlkxx3pGKQrfkcK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bBbHAP/btqxqWMUMam/tDakW5Gnlkxx3pGKQrfkcK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bBbHAP/btqxqWMUMam/tDakW5Gnlkxx3pGKQrfkcK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbBbHAP%2FbtqxqWMUMam%2FtDakW5Gnlkxx3pGKQrfkcK%2Fimg.png&quot; data-filename=&quot;3.png&quot; width=&quot;197&quot; height=&quot;119&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3&gt;생각해보기&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;한 디렉토리에 저장되는 파일의 수는 제한이 있습니다&lt;span&gt;. &lt;/span&gt;파일 업로드를 구현할 때는 파일이 업로드되는 디렉토리의 구조도 중요합니다&lt;span&gt;. &lt;/span&gt;업로드되는 파일을 저장하기 위한 디렉토리 구조를 어떻게 하는 것이 좋을까요&lt;span&gt;? (&lt;/span&gt;힌트&lt;span&gt; : &lt;/span&gt;파일이 업로드되는 날짜 정보를 이용한다&lt;span&gt;.)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;MultipartFile#transferTo() &lt;/span&gt;를 이용해서 실습 코드를 수정해보세요&lt;span&gt;.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;참고 자료&lt;/h3&gt;
&lt;p&gt;&lt;b&gt;&lt;span style=&quot;color: #333333;&quot;&gt;[&lt;/span&gt;&lt;/b&gt;&lt;b&gt;&lt;span style=&quot;color: #333333;&quot;&gt;참고링크&lt;span&gt;] Spring MVC File Upload Example Tutorial - Single and Multiple Files - JournalDev&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&lt;a href=&quot;https://www.journaldev.com/2573/spring-mvc-file-upload-example-single-multiple-files&quot;&gt;https://www.journaldev.com/2573/spring-mvc-file-upload-example-single-multiple-files&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;b&gt;&lt;span style=&quot;color: #333333;&quot;&gt;[&lt;/span&gt;&lt;/b&gt;&lt;b&gt;&lt;span style=&quot;color: #333333;&quot;&gt;참고링크&lt;span&gt;] FileUpload &lt;/span&gt;&amp;ndash;&lt;span&gt; Home&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&lt;a href=&quot;https://commons.apache.org/proper/commons-fileupload/&quot;&gt;https://commons.apache.org/proper/commons-fileupload/&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;</description>
      <category>부스트코스 웹 프로그래밍/6. 웹 앱 개발: 예약서비스 4</category>
      <category>부스트코스</category>
      <category>웹프로그래밍</category>
      <author>haaak</author>
      <guid isPermaLink="true">https://nbkw.tistory.com/164</guid>
      <comments>https://nbkw.tistory.com/164#entry164comment</comments>
      <pubDate>Tue, 13 Aug 2019 15:26:15 +0900</pubDate>
    </item>
    <item>
      <title>3. 파일 업로드 &amp;amp; 다운로드 - BE (1)</title>
      <link>https://nbkw.tistory.com/163</link>
      <description>&lt;h2&gt;&lt;span&gt;1) &lt;/span&gt;파일업로드 컨셉설명&lt;/h2&gt;
&lt;h3&gt;&lt;span&gt;Multipart?&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;1.png&quot; width=&quot;462&quot; height=&quot;373&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bQzjlY/btqxtWYXNNd/5fvnCD4Y2NpFDQ8t5VkxLk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bQzjlY/btqxtWYXNNd/5fvnCD4Y2NpFDQ8t5VkxLk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bQzjlY/btqxtWYXNNd/5fvnCD4Y2NpFDQ8t5VkxLk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbQzjlY%2FbtqxtWYXNNd%2F5fvnCD4Y2NpFDQ8t5VkxLk%2Fimg.png&quot; data-filename=&quot;1.png&quot; width=&quot;462&quot; height=&quot;373&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Multipart는&amp;nbsp;&lt;span style=&quot;color: #333333;&quot;&gt;보통 파일을 전송할 때 사용합니다&lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;웹 클라이언트가 요청을 보낼 때&lt;span&gt; HTTP&lt;/span&gt;프로토콜의 바디 부분에 데이터를 여러 부분으로 나눠서 보내며, 그림과 같이&amp;nbsp;&lt;span&gt;Content-Type, &lt;/span&gt;파일 이름&lt;span&gt;, &lt;/span&gt;파일 정보 등으로 나뉜 부분을 연결하여 전송합니다&lt;span&gt;.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;이렇게 여러 부분으로 나눠서 오는 것을&lt;span&gt; multipart &lt;/span&gt;데이터라고 합니다&lt;span&gt;.&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;span&gt;HttpServletRequest&lt;/span&gt;는 파일 업로드를 지원 안 함&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;HttpServletRequest&lt;/span&gt;는 웹 클라이언트가 전달하는&lt;span&gt; Multipart&lt;/span&gt;데이터를 쉽게 처리하는 메소드를 제공하지 않습니다&lt;span&gt;.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;서블릿에서 파일 업로드를 처리하려면 별도의 라이브러리를 사용해야 합니다&lt;span&gt;.&amp;nbsp;&lt;/span&gt;대표적인 라이브러리가 아파치 재단의&lt;span&gt; commons-fileupload&lt;/span&gt;입니다&lt;span&gt;.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;HttpServletRequest&lt;/span&gt;는&lt;span&gt; HTTP &lt;/span&gt;프로토콜의 바디 부분을 읽어들이는 &lt;span&gt;InputStream&lt;/span&gt;만을 지원하기에 사용자는 이런&lt;span&gt; InputStream&lt;/span&gt;을 이용해서 &lt;span&gt;multipart &lt;/span&gt;부분을 잘 나눠서 사용을 해야 됩니다&lt;span&gt;.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;span&gt;Spring MVC&lt;/span&gt;에서의 파일 업로드&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;Spring MVC&lt;/span&gt;에서 파일을 업로드 하려면 몇 가지 라이브러리와 설정을 추가해야 합니다&lt;span&gt;.&lt;br /&gt;- commons-fileupload, commons-io &lt;/span&gt;라이브러리 추가&lt;span&gt;&lt;br /&gt;- MultipartResolver Bean &lt;/span&gt;추가&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;파일 업로드를 위한 라이브러리 추가&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;pom.xml&lt;/span&gt;에 아래의 &lt;span&gt;dependency&lt;/span&gt;를 추가합니다&lt;span&gt;.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td width=&quot;566&quot;&gt;
&lt;p&gt;&lt;span&gt;&amp;lt;dependency&amp;gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&amp;lt;groupId&amp;gt;commons-fileupload&amp;lt;/groupId&amp;gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&amp;lt;artifactId&amp;gt;commons-fileupload&amp;lt;/artifactId&amp;gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&amp;lt;version&amp;gt;1.2.1&amp;lt;/version&amp;gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&amp;lt;/dependency&amp;gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&amp;lt;dependency&amp;gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&amp;lt;groupId&amp;gt;commons-io&amp;lt;/groupId&amp;gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&amp;lt;artifactId&amp;gt;commons-io&amp;lt;/artifactId&amp;gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&amp;lt;version&amp;gt;1.4&amp;lt;/version&amp;gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&amp;lt;/dependency&amp;gt;&lt;/span&gt;&lt;/p&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;h3&gt;스프링 설정 추가&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;DispathcerServlet&lt;/span&gt;은 준비 과정에서 &lt;span&gt;&quot;multipart/form-data&quot;&lt;/span&gt;가 요청으로 올 경우&lt;span&gt; MultipartResolver&lt;/span&gt;를 사용합니다&lt;span&gt;.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;이를 위해서&lt;span&gt; MultipartResolver &lt;/span&gt;객체를 생성하는&lt;span&gt; Bean&lt;/span&gt;을 설정해줘야 합니다&lt;span&gt;.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td width=&quot;566&quot;&gt;
&lt;p&gt;&lt;span&gt;@Bean&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;public MultipartResolver multipartResolver() {&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&amp;nbsp; org.springframework.web.multipart.commons.CommonsMultipartResolver multipartResolver = new org.springframework.web.multipart.commons.CommonsMultipartResolver();&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&amp;nbsp; multipartResolver.setMaxUploadSize(10485760); // 1024 * 1024 * 10&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&amp;nbsp; return multipartResolver;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;}&lt;/span&gt;&lt;/p&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;h3&gt;파일 업로드 폼&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;파일 업로드 시에는&lt;span&gt; form&lt;/span&gt;태그에&lt;span&gt; enctype(multipart/ form-data)&lt;/span&gt;설정이 되어 있어야 합니다&lt;span&gt;.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;post &lt;/span&gt;방식으로 전송해야 합니다&lt;span&gt;.&amp;nbsp;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;input &lt;/span&gt;태그의&lt;span&gt; type&lt;/span&gt;은 &lt;span&gt;file&lt;/span&gt;로 지정해야 합니다&lt;span&gt;.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;type&lt;/span&gt;이&lt;span&gt; file&lt;/span&gt;인&lt;span&gt; input &lt;/span&gt;태그가 여러 개 있고&lt;span&gt; name &lt;/span&gt;속성의 값이 같다면 &lt;span&gt;file&lt;/span&gt;이 배열 형태로 컨트롤러에게 전달이 되게 됩니다&lt;span&gt;.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td width=&quot;566&quot;&gt;
&lt;p&gt;&lt;span&gt;&amp;lt;form method=&quot;post&quot; action=&quot;/upload&quot;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/span&gt;enctype=&quot;multipart/form-data&quot;&amp;gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;......&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&amp;lt;input type=&quot;file&quot; name=&quot;file&quot;&amp;gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&amp;lt;input type=&quot;submit&quot;&amp;gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&amp;lt;/form&amp;gt;&lt;/span&gt;&lt;/p&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;h3&gt;&lt;span&gt;Controller&lt;/span&gt;에서의 업로드 처리&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;폼으로부터 전달되는 파일 정보를 받아들이는&lt;span&gt; Controller&lt;/span&gt;를 추가를 해야합니다&lt;span&gt;. &lt;/span&gt;파일 정보는 파라미터로 전달되며 이 때&lt;span&gt; MultipartFile type&lt;/span&gt;으로 전달이 되게 됩니다&lt;span&gt;.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;@PostMapping&lt;/span&gt;이 사용되어야 합니다&lt;span&gt;.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;업로드 파일이 하나일 경우&lt;span&gt; @RequestParam(&quot;file&quot;) MultipartFile file&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;업로드 파일이 여러 개일 경우&lt;span&gt; @RequestParam(&quot;file&quot;) MultipartFile[] files (&lt;/span&gt;배열로 사용&lt;span&gt;)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;MultipartFile&lt;/span&gt;의 메소드를 이용해서 파일 이름&lt;span&gt;, &lt;/span&gt;파일 크기 등을 구하고&lt;span&gt; InputStream&lt;/span&gt;을 얻어 파일을 서버에 저장합니다&lt;span&gt;.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;파일은 서버의 파일 시스템에 저장하고 파일 정보는 &lt;span&gt;DB&lt;/span&gt;에 저장합니다&lt;span&gt;.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;span&gt;Controller&lt;/span&gt;에서의 다운로드 처리&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서버의 특정 디렉토리는 외부에서 접근할 수가 없기에 파일을 외부에서 사용할 수 있도록 하려면 다운로드 기능을 제공해야 됩니다&lt;span&gt;.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;파일 다운로드와 관련된 헤더 정보를 출력합니다&lt;span&gt;.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td width=&quot;566&quot;&gt;
&lt;p&gt;&lt;span&gt;response.setHeader(&quot;Content-Disposition&quot;, &quot;attachment; filename=\&quot;&quot; + fileName + &quot;\&quot;;&quot;);&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;response.setHeader(&quot;Content-Transfer-Encoding&quot;, &quot;binary&quot;);&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;response.setHeader(&quot;Content-Type&quot;, contentType);&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;response.setHeader(&quot;Content-Length&quot;, fileLength;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;response.setHeader(&quot;Pragma&quot;, &quot;no-cache;&quot;);&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;response.setHeader(&quot;Expires&quot;, &quot;-1;&quot;);&lt;/span&gt;&lt;/p&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;파일을 다운로드하는&lt;span&gt; Controller &lt;/span&gt;메서드에서는 먼저 헤더 정보에 사용자가 다운 받으려는 파일 정보와 함께 캐시를 사용하지 않도록 설정을 해줘야 됩니다&lt;span&gt;. &lt;/span&gt;그리고 파일 정보를&lt;span&gt; HttpServlet Response&lt;/span&gt;의&lt;span&gt; OutputStream&lt;/span&gt;을 이용해서 출력해야 사용할 수 있습니다&lt;span&gt;.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;파일을 읽어&lt;span&gt; HttpServletResponse&lt;/span&gt;의&lt;span&gt; OutputStream&lt;/span&gt;으로 출력합니다&lt;span&gt;.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;생각해보기&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;HTTP&lt;/span&gt;프로토콜의 바디 부분에 스트림&lt;span&gt;(stream)&lt;/span&gt;방식으로 전달되는&lt;span&gt; Multipart&lt;/span&gt;를 읽어들이기 위해 아파치 재단의&lt;span&gt; commons-fileuplaod&lt;/span&gt;라이브러리를 이용한다고 했습니다&lt;span&gt;. Multipart&lt;/span&gt;형식에 대해 좀 더 상세히 알아보고&lt;span&gt;, &lt;/span&gt;이를 직접 파싱하려면 어떻게 해야 할지 생각해보세요&lt;span&gt;.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;참고 자료&lt;/h3&gt;
&lt;p&gt;&lt;b&gt;&lt;span style=&quot;color: #333333;&quot;&gt;[&lt;/span&gt;&lt;/b&gt;&lt;b&gt;&lt;span style=&quot;color: #333333;&quot;&gt;참고링크&lt;span&gt;] FileUpload &lt;/span&gt;&amp;ndash;&lt;span&gt; Home&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&lt;a href=&quot;https://commons.apache.org/proper/commons-fileupload/&quot;&gt;https://commons.apache.org/proper/commons-fileupload/&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;b&gt;&lt;span style=&quot;color: #333333;&quot;&gt;[&lt;/span&gt;&lt;/b&gt;&lt;b&gt;&lt;span style=&quot;color: #333333;&quot;&gt;참고링크&lt;span&gt;] Convenient way to parse incoming multipart/form-data parameters in a Servlet&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&lt;a href=&quot;https://stackoverflow.com/questions/3337056/convenient-way-to-parse-incoming-multipart-form-data-parameters-in-a-servlet&quot;&gt;https://stackoverflow.com/questions/3337056/convenient-way-to-parse-incoming-multipart-form-data-parameters-in-a-servlet&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;</description>
      <category>부스트코스 웹 프로그래밍/6. 웹 앱 개발: 예약서비스 4</category>
      <category>부스트코스</category>
      <category>웹프로그래밍</category>
      <author>haaak</author>
      <guid isPermaLink="true">https://nbkw.tistory.com/163</guid>
      <comments>https://nbkw.tistory.com/163#entry163comment</comments>
      <pubDate>Tue, 13 Aug 2019 15:09:54 +0900</pubDate>
    </item>
  </channel>
</rss>