Repository: arukuArupaka/arupaka-db-manager
Pull request #14: Feat/listingitem dto
Author: login: hyuuk1
Created: 2025-03-03T07:57:15Z | Merged: 2025-03-10T14:48:58Z

# 目的や背景

<!--
この Pull Request の目的や背景を記載してください。
目的や背景を記述することで、実装方針や解決方法が間違っていないか他の人も確認できるようにします。
-->

classvalidatorの導入と、imageurlsテーブルの追加

# 変更内容の概要

<!--
変更内容の概要を記載してください。コードを見れば変更内容がすぐにわかるようであれば、詳細に書く必要はありません。
-->
classvalidatorの導入により、入力されたデータが不正だった場合には、エラーを伝えるオブジェクトが返されます
{message:string,error:string,statuscode:number}。
listingitemテーブルがimageurl:string[]を持っていたところを、imageurl:{id,url,listingitemId}というテーブルを別に作り、リレーションが作成されました。それに合わせて、create処理を書き替えました。
# スクリーンショット

<!--
デザインに関係する修正や、複雑な操作の説明が必要な場合に、修正対象の箇所の画像・動画を添付してください。
-->

# 関連するリンク

<!--
この Pull Request (PR) と関連する他の PR のリンクや notion ページのリンクがあれば貼ってください。
-->

# Staging 環境での動作確認チェックリスト

<!--
レビューを受ける前に、Staging 環境での動作確認のチェックリストを記載してください。
レビューワーがチェックリストを見ることで、確認事項の過不足がないかを開発メンバー同士で確認できます。
また、リリース後に不具合が起きたとき、このチェックリストを振り返ることで、不具合が起きた箇所や確認不足だった箇所を把握しやすくします。
-->

- [ ] ここに確認事項を記載



Diff:
diff --git a/nestJS/package-lock.json b/nestJS/package-lock.json
index 9d3a00b..229fcbb 100644
--- a/nestJS/package-lock.json
+++ b/nestJS/package-lock.json
@@ -6153,9 +6153,9 @@
       }
     },
     "node_modules/libphonenumber-js": {
-      "version": "1.11.20",
-      "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.11.20.tgz",
-      "integrity": "sha512-/ipwAMvtSZRdiQBHqW1qxqeYiBMzncOQLVA+62MWYr7N4m7Q2jqpJ0WgT7zlOEOpyLRSqrMXidbJpC0J77AaKA==",
+      "version": "1.12.4",
+      "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.12.4.tgz",
+      "integrity": "sha512-vLmhg7Gan7idyAKfc6pvCtNzvar4/eIzrVVk3hjNFH5+fGqyjD0gQRovdTrDl20wsmZhBtmZpcsR0tOfquwb8g==",
       "license": "MIT"
     },
     "node_modules/lines-and-columns": {
diff --git a/nestJS/src/common/validate-value.ts b/nestJS/src/common/validate-value.ts
new file mode 100644
index 0000000..b408045
--- /dev/null
+++ b/nestJS/src/common/validate-value.ts
@@ -0,0 +1,16 @@
+import { plainToInstance } from "class-transformer";
+import { validate } from "class-validator";
+
+export async function validateValue<T extends object>(
+  cls: new () => T,
+  plain: object
+): Promise<T> {
+  const instance = plainToInstance(cls, plain);
+
+  const errors = await validate(instance);
+  if (errors.length > 0) {
+    throw new Error(errors.toString());
+  }
+
+  return instance;
+}
\ No newline at end of file
diff --git a/nestJS/src/listing-item/interface/listing-item-create.input.ts b/nestJS/src/listing-item/interface/listing-item-create.input.ts
new file mode 100644
index 0000000..91cfe56
--- /dev/null
+++ b/nestJS/src/listing-item/interface/listing-item-create.input.ts
@@ -0,0 +1,36 @@
+import { Academics, Condition } from '@prisma/client';
+import {
+  ArrayNotEmpty,
+  ArrayUnique,
+  IsArray,
+  IsDate,
+  IsEnum,
+  IsInt,
+  IsOptional,
+  IsString,
+  Min,
+} from 'class-validator';
+
+export class ListingItemCreateInput {
+  @IsString()
+  documentId: string; //FirebaseにおけるID
+  @IsEnum(Condition)
+  condition: Condition; //商品の状態
+  @IsEnum(Academics)
+  department: Academics; //学部
+  @IsOptional()
+  @IsString()
+  description?: string; //商品の説明
+  @IsArray()
+  @ArrayNotEmpty()
+  @ArrayUnique()
+  @IsString({each:true})
+  imageUrls:string[];
+  @IsInt()
+  @Min(0)
+  price: number; //商品の価格
+  @IsString()
+  name: string; //商品
+  @IsString()
+  firebaseUserId: string; //ユーザーID
+}
diff --git a/nestJS/src/listing-item/interface/listing-item.payload.ts b/nestJS/src/listing-item/interface/listing-item.payload.ts
index cc5a434..ee00ca2 100644
--- a/nestJS/src/listing-item/interface/listing-item.payload.ts
+++ b/nestJS/src/listing-item/interface/listing-item.payload.ts
@@ -1,42 +1,46 @@
-export interface RecievedListingItemPayload {
+import { Academics, Condition } from '@prisma/client';
+import {
+  ArrayNotEmpty,
+  ArrayUnique,
+  IsArray,
+  IsDate,
+  IsEnum,
+  IsInt,
+  IsOptional,
+  IsString,
+  Min,
+} from 'class-validator';
+
+export class ListingItemPayload {
+  @IsString()
   documentId: string; //FirebaseにおけるID
+  @IsOptional()
+  @IsDate()
   purchasedAt?: Date; //購入した日時
+  @IsOptional()
+  @IsString()
   purchasedUserId?: string; //購入したユーザーのid
+  @IsEnum(Condition)
   condition: Condition; //商品の状態
+  @IsEnum(Academics)
   department: Academics; //学部
+  @IsOptional()
+  @IsString()
   description?: string; //商品の説明
-  imageUrl: string[]; //商品の画像のURL
+  @IsArray()
+  @ArrayNotEmpty()
+  @ArrayUnique()
+  @IsString({each:true})
+  imageUrls:string[];
+  @IsInt()
+  @Min(0)
   price: number; //商品の価格
+  @IsString()
   name: string; //商品
+  @IsString()
   firebaseUserId: string; //ユーザーID
-}
-
-export interface ListingItemPayload extends RecievedListingItemPayload {
+  @IsInt()
   id: number; //ID
+  @IsDate()
   createdAt: Date; //出品された日時
-}
-export type Condition =
-  | 'BRAND_NEW' //新品、未使用
-  | 'LIKE_NEW' //未使用に近い
-  | 'GOOD' //目立った傷や汚れなし
-  | 'FAIR' //やや傷や汚れあり
-  | 'POOR' //傷や汚れあり
-  | 'BAD'; //全体的に状態が悪い
-
-export type Academics =
-  | 'Law' // 法学部
-  | 'SocialSciences' // 社会学部
-  | 'InternationalRelations' // 国際関係学部
-  | 'Letters' // 文学部
-  | 'Economics' // 経済学部
-  | 'SportandHealthScience' // スポーツ健康科学部
-  | 'ScienceandEngineering' // 理工学部
-  | 'LifeSciences' // 生命科学部
-  | 'PharmaceuticalSciences' // 薬学部
-  | 'GastronomyManagement' // 食マネジメント学部
-  | 'BusinessAdministration' // 経営学部
-  | 'PolicyScience' // 政策科学部
-  | 'ComprehensivePsychology' // 総合心理学部
-  | 'GlobalLiberalArts' // グローバル教養学部
-  | 'ImageArtsandSciences' // 映像学部
-  | 'InformationScienceandEngineering'; // 情報理工学部
+}
\ No newline at end of file
diff --git a/nestJS/src/listing-item/interface/listingitem-search.input.ts b/nestJS/src/listing-item/interface/listingitem-search.input.ts
new file mode 100644
index 0000000..1a8de32
--- /dev/null
+++ b/nestJS/src/listing-item/interface/listingitem-search.input.ts
@@ -0,0 +1,8 @@
+import { Type } from "class-transformer";
+import { IsString } from "class-validator";
+
+export class ListingItemSearchInput {
+    @IsString()
+    @Type(()=>String)
+    name:string
+}
\ No newline at end of file
diff --git a/nestJS/src/listing-item/listing-item.controller.ts b/nestJS/src/listing-item/listing-item.controller.ts
index 9fe2c8b..b49569a 100644
--- a/nestJS/src/listing-item/listing-item.controller.ts
+++ b/nestJS/src/listing-item/listing-item.controller.ts
@@ -2,23 +2,23 @@ import { Body, Controller, Get, Param, Post, Query } from '@nestjs/common';
 import { SearchItemService } from './listing-item.service';
 import {
   ListingItemPayload,
-  RecievedListingItemPayload,
 } from './interface/listing-item.payload';
-import { CustomPrismaService } from 'src/prisma/prisma.service';
+import { ListingItemSearchInput } from './interface/listingitem-search.input';
+import { ListingItemCreateInput } from './interface/listing-item-create.input';
 
 @Controller('listing_item')
 export class SearchItemController {
   constructor(private searchItemService: SearchItemService) {}
 
   @Get('search_item')
-  async getItems(@Query('name') name: string): Promise<ListingItemPayload[]> {
-    return this.searchItemService.findItems(name);
+  async searchItems(@Query() query:ListingItemSearchInput):Promise<ListingItemPayload[]> {
+    return this.searchItemService.searchItems(query);
   }
 
   @Post('create_item')
-  async CreateItem(
-    @Body() createItem: RecievedListingItemPayload,
+  async createItem(
+    @Body() createItem:ListingItemCreateInput,
   ): Promise<string> {
-    return this.searchItemService.ItemCreate(createItem);
+    return this.searchItemService.createItem(createItem);
   }
 }
diff --git a/nestJS/src/listing-item/listing-item.service.ts b/nestJS/src/listing-item/listing-item.service.ts
index a6bd235..3b9f8d5 100644
--- a/nestJS/src/listing-item/listing-item.service.ts
+++ b/nestJS/src/listing-item/listing-item.service.ts
@@ -1,29 +1,79 @@
-import { Injectable } from '@nestjs/common';
+import { BadRequestException, Injectable } from '@nestjs/common';
 import {
   ListingItemPayload,
-  RecievedListingItemPayload,
 } from './interface/listing-item.payload';
 import { CustomPrismaService } from 'src/prisma/prisma.service';
+import { validateValue } from 'src/common/validate-value';
+import { ListingItemSearchInput } from './interface/listingitem-search.input';
+import { ListingItemCreateInput } from './interface/listing-item-create.input';
 
 @Injectable()
 export class SearchItemService {
   constructor(private readonly prisma: CustomPrismaService) {}
 
-  async findItems(searchWord: string): Promise<ListingItemPayload[]> {
+  async searchItems(query: ListingItemSearchInput): Promise<ListingItemPayload[]> {
     const items = await this.prisma.listingItem.findMany({
       where: {
-        name: { contains: searchWord },
+        name: { contains: query.name },
       },
-    });
-    return items;
+      //全部select(imageurl含め)
+      select:{
+        id:true,
+        documentId:true,
+        purchasedAt:true,
+        purchasedUserId:true,
+        condition:true,
+        createdAt:true,
+        department:true,
+        description:true,
+        price:true,
+        name:true,
+        firebaseUserId:true,
+        imageUrls:true
+      }});
+    const newItems = await Promise.all(items.map((item) => {
+      const newItem:ListingItemPayload = {
+        id: item.id,
+        createdAt: item.createdAt,
+        documentId: item.documentId,
+        condition: item.condition,
+        department: item.department,
+        imageUrls: item.imageUrls.map((imageUrl)=>{return imageUrl.url}),
+        price: item.price,
+        name: item.name,
+        firebaseUserId: item.firebaseUserId};
+      const checkedItem = validateValue(ListingItemPayload,newItem);  
+      return checkedItem;
+    }));
+    return newItems;
   }
 
-  async ItemCreate(createItem: RecievedListingItemPayload): Promise<string> {
-    await this.prisma.listingItem.upsert({
-      where: { documentId: createItem.documentId },
-      update: createItem,
-      create: createItem,
+  async createItem(createItem:ListingItemCreateInput): Promise<string> {
+    const imageUrls = await this.prisma.imageUrl.findMany({
+      where:{
+        url:{
+          in:createItem.imageUrls
+        }
+      }
+    });
+    if (imageUrls.length > 0){
+      throw new BadRequestException();
+    }
+    const adjustedCreateItem = {
+      //image配列をimageテーブルのオブジェクトの配列に変更
+      documentId: createItem.documentId,
+      condition: createItem.condition,
+      department: createItem.department,
+      price: createItem.price,
+      name: createItem.name,
+      firebaseUserId: createItem.firebaseUserId,
+      imageUrls:{
+      create:createItem.imageUrls.map((imageUrl)=>({url:imageUrl}))
+      } 
+    }
+    await this.prisma.listingItem.create({
+      data:adjustedCreateItem,
     });
     return 'OK';
   }
-}
+}
\ No newline at end of file


Reviews:
user: login: sho-ritz
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-04T09:25:03Z
user: login: hyuuk1
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-07T10:31:24Z
user: login: sho-ritz
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-08T20:02:28Z
user: login: sho-ritz
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-09T13:11:41Z
user: login: hyuuk1
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-09T14:59:32Z
user: login: hyuuk1
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-10T09:16:03Z
user: login: hyuuk1
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-10T09:16:44Z
user: login: hyuuk1
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-10T09:17:12Z

Review comments:
diff_hunk: @@ -14,16 +15,55 @@ export class SearchItemService {
       where: {
         name: { contains: searchWord },
       },
+      //全部select(imageurl含め)
+      select:{id:true,documentId:true,purchasedAt:true,purchasedUserId:true,condition:true,createdAt:true,department:true,description:true,price:true,name:true,firebaseUserId:true,imageUrls:true}
     });
-    return items;
+    const newitems = items.map((item) => {
+      const newitem:ListingItemPayload = {
+        id: item.id,
+        createdAt: item.createdAt,
+        documentId: item.documentId,
+        condition: item.condition,
+        department: item.department,
+        imageUrls: item.imageUrls.map((imageUrl)=>{return imageUrl.url}),
+        price: item.price,
+        name: item.name,
+        firebaseUserId: item.firebaseUserId};
+      return newitem;
+    })
+    return newitems;
   }
 
   async ItemCreate(createItem: RecievedListingItemPayload): Promise<string> {
+    validate(createItem).then((errors)=>{
path: nestJS/src/listing-item/listing-item.service.ts
user: login: sho-ritz
body: この書き方だと、`class-validator`を使ってる全部のメソッド内に`validate( ~ ).then( ~~`て書かなくちゃいけなくなって冗長性が増しちゃう。
だから別で定義した方がよさそう。
俺の今やってる別のプロジェクトだと下の関数を新たに定義して、それを各メソッド内で呼び出して使ってる。
```
import { validate } from "class-validator";
import { plainToInstance } from "class-transformer";

export async function validateInput<T extends object>(
  cls: new () => T,
  plain: object
): Promise<T> {
  const instance = plainToInstance(cls, plain);

  const errors = await validate(instance);
  if (errors.length > 0) {
    throw new Error(errors.toString());
  }

  return instance;
}

```

引数をジェネリクスで定義してるからどんな型にも対応できるようになってる。
呼び出すときは、
```
const validatedInput = await validateInput(<チェックしたいDTO>, <inputの値>);
```
だけでチェックできて、そのあとの処理は`validatedInput`を使ってやれば、DTOで変換済みの変数を使えるから型安全性が上がる。
created_at: 2025-03-04T09:11:57Z
author_association: COLLABORATOR
diff_hunk: @@ -14,16 +15,55 @@ export class SearchItemService {
       where: {
         name: { contains: searchWord },
       },
+      //全部select(imageurl含め)
+      select:{id:true,documentId:true,purchasedAt:true,purchasedUserId:true,condition:true,createdAt:true,department:true,description:true,price:true,name:true,firebaseUserId:true,imageUrls:true}
path: nestJS/src/listing-item/listing-item.service.ts
user: login: sho-ritz
body: 読みづらいからインデントをそろえて欲しい
created_at: 2025-03-04T09:23:30Z
author_association: COLLABORATOR
diff_hunk: @@ -4,21 +4,21 @@ import {
   ListingItemPayload,
   RecievedListingItemPayload,
 } from './interface/listing-item.payload';
-import { CustomPrismaService } from 'src/prisma/prisma.service';
+import { ValidationError } from 'class-validator';
 
 @Controller('listing_item')
 export class SearchItemController {
   constructor(private searchItemService: SearchItemService) {}
 
   @Get('search_item')
-  async getItems(@Query('name') name: string): Promise<ListingItemPayload[]> {
+  async getItems(@Query('name') name: string):Promise<ListingItemPayload[]> {
     return this.searchItemService.findItems(name);
   }
 
   @Post('create_item')
   async CreateItem(
     @Body() createItem: RecievedListingItemPayload,
-  ): Promise<string> {
+  ): Promise<string|ValidationError[]> {
path: nestJS/src/listing-item/listing-item.controller.ts
user: login: sho-ritz
body: validateを修正したらこの型入らないから消してほしい
created_at: 2025-03-04T09:24:37Z
author_association: COLLABORATOR
diff_hunk: @@ -4,21 +4,21 @@ import {
   ListingItemPayload,
   RecievedListingItemPayload,
 } from './interface/listing-item.payload';
-import { CustomPrismaService } from 'src/prisma/prisma.service';
+import { ValidationError } from 'class-validator';
 
 @Controller('listing_item')
 export class SearchItemController {
   constructor(private searchItemService: SearchItemService) {}
 
   @Get('search_item')
-  async getItems(@Query('name') name: string): Promise<ListingItemPayload[]> {
+  async getItems(@Query('name') name: string):Promise<ListingItemPayload[]> {
     return this.searchItemService.findItems(name);
   }
 
   @Post('create_item')
   async CreateItem(
     @Body() createItem: RecievedListingItemPayload,
-  ): Promise<string> {
+  ): Promise<string|ValidationError[]> {
path: nestJS/src/listing-item/listing-item.controller.ts
user: login: hyuuk1
body: [511a5ae](https://github.com/arukuArupaka/arupaka-db-manager/pull/14/commits/511a5aef4efb034578ab7bbc91cd3ac92d902da1)
created_at: 2025-03-07T10:31:24Z
author_association: COLLABORATOR
diff_hunk: @@ -1,15 +1,88 @@
+import { plainToInstance } from 'class-transformer';
 import {
   ArrayNotEmpty,
   ArrayUnique,
   IsArray,
   IsDate,
   IsFQDN,
+  IsIn,
   IsInt,
   IsOptional,
   IsString,
   Min,
+  validate
 } from 'class-validator';
 
+export async function validateInput<T extends object>(
+  cls: new () => T,
+  plain: object
+): Promise<T> {
+  const instance = plainToInstance(cls, plain);
+
+  const errors = await validate(instance);
+  if (errors.length > 0) {
+    throw new Error(errors.toString());
+  }
+
+  return instance;
+}
path: nestJS/src/listing-item/interface/listing-item.payload.ts
user: login: sho-ritz
body: この`validate-input`全サービス内で今後使うだろうから、`listing-item`とかと同じディレクトリに新しく`common`っていうディレクトリを作ってその中に作ってほしい
created_at: 2025-03-08T19:59:50Z
author_association: COLLABORATOR
diff_hunk: @@ -35,35 +48,34 @@ export class SearchItemService {
   }
 
   async ItemCreate(createItem: RecievedListingItemPayload): Promise<string> {
path: nestJS/src/listing-item/listing-item.service.ts
user: login: sho-ritz
body: 引数の型が`Payload`なのは何か変じゃない？

created_at: 2025-03-08T20:02:05Z
author_association: COLLABORATOR
diff_hunk: @@ -1,42 +1,121 @@
-export interface RecievedListingItemPayload {
+import { plainToInstance } from 'class-transformer';
+import {
+  ArrayNotEmpty,
+  ArrayUnique,
+  IsArray,
+  IsDate,
+  IsFQDN,
+  IsIn,
+  IsInt,
+  IsOptional,
+  IsString,
+  Min,
+  validate
+} from 'class-validator';
+
+export async function validateInput<T extends object>(
+  cls: new () => T,
+  plain: object
+): Promise<T> {
+  const instance = plainToInstance(cls, plain);
+
+  const errors = await validate(instance);
+  if (errors.length > 0) {
+    throw new Error(errors.toString());
+  }
+
+  return instance;
+}
+
+const allConditions = [
+'BRAND_NEW',
+'LIKE_NEW',
+'GOOD',
+'FAIR',
+'POOR',
+'BAD'];
+
+export type Condition =
+  | 'BRAND_NEW' //新品、未使用
+  | 'LIKE_NEW' //未使用に近い
+  | 'GOOD' //目立った傷や汚れなし
+  | 'FAIR' //やや傷や汚れあり
+  | 'POOR' //傷や汚れあり
+  | 'BAD'; //全体的に状態が悪い
+
+const allAcademics = [
+  'All'
+  ,'LiberalArts'
+  ,'Law'
+  ,'SocialSciences'
+  ,'InternationalRelations'
+  ,'Literature'
+  ,'Business'
+  ,'PolicyScience'
+  ,'Psychology'
+  ,'GlobalLiberalArts'
+  ,'Film'
+  ,'InformationScience'
+  ,'ScienceAndTechnology'
+  ,'Economics'
+  ,'SportsHealthScience'
+  ,'FoodManagement'
+  ,'LifeSciences'
+  ,'Pharmacy'];
+
+export type Academics =
+  | 'All'
+  | 'LiberalArts'
+  | 'Law'
+  | 'SocialSciences'
+  | 'InternationalRelations'
+  | 'Literature'
+  | 'Business'
+  | 'PolicyScience'
+  | 'Psychology'
+  | 'GlobalLiberalArts'
+  | 'Film'
+  | 'InformationScience'
+  | 'ScienceAndTechnology'
+  | 'Economics'
+  | 'SportsHealthScience'
+  | 'FoodManagement'
+  | 'LifeSciences'
+  | 'Pharmacy';
+
+export class RecievedListingItemPayload {
+  @IsString()
   documentId: string; //FirebaseにおけるID
+  @IsOptional()
+  @IsDate()
   purchasedAt?: Date; //購入した日時
+  @IsOptional()
+  @IsString()
   purchasedUserId?: string; //購入したユーザーのid
+  @IsIn(allConditions)
path: nestJS/src/listing-item/interface/listing-item.payload.ts
user: login: sho-ritz
body: `@IsIn`でもいいんだけど、`@IsEnum(Conditions)`でもいいかも
created_at: 2025-03-09T13:11:36Z
author_association: COLLABORATOR
diff_hunk: @@ -35,35 +48,34 @@ export class SearchItemService {
   }
 
   async ItemCreate(createItem: RecievedListingItemPayload): Promise<string> {
path: nestJS/src/listing-item/listing-item.service.ts
user: login: hyuuk1
body: ペイロードは荷物的な意味なんで大丈夫と思います
created_at: 2025-03-09T14:59:32Z
author_association: COLLABORATOR
diff_hunk: @@ -1,42 +1,121 @@
-export interface RecievedListingItemPayload {
+import { plainToInstance } from 'class-transformer';
+import {
+  ArrayNotEmpty,
+  ArrayUnique,
+  IsArray,
+  IsDate,
+  IsFQDN,
+  IsIn,
+  IsInt,
+  IsOptional,
+  IsString,
+  Min,
+  validate
+} from 'class-validator';
+
+export async function validateInput<T extends object>(
+  cls: new () => T,
+  plain: object
+): Promise<T> {
+  const instance = plainToInstance(cls, plain);
+
+  const errors = await validate(instance);
+  if (errors.length > 0) {
+    throw new Error(errors.toString());
+  }
+
+  return instance;
+}
+
+const allConditions = [
+'BRAND_NEW',
+'LIKE_NEW',
+'GOOD',
+'FAIR',
+'POOR',
+'BAD'];
+
+export type Condition =
+  | 'BRAND_NEW' //新品、未使用
+  | 'LIKE_NEW' //未使用に近い
+  | 'GOOD' //目立った傷や汚れなし
+  | 'FAIR' //やや傷や汚れあり
+  | 'POOR' //傷や汚れあり
+  | 'BAD'; //全体的に状態が悪い
+
+const allAcademics = [
+  'All'
+  ,'LiberalArts'
+  ,'Law'
+  ,'SocialSciences'
+  ,'InternationalRelations'
+  ,'Literature'
+  ,'Business'
+  ,'PolicyScience'
+  ,'Psychology'
+  ,'GlobalLiberalArts'
+  ,'Film'
+  ,'InformationScience'
+  ,'ScienceAndTechnology'
+  ,'Economics'
+  ,'SportsHealthScience'
+  ,'FoodManagement'
+  ,'LifeSciences'
+  ,'Pharmacy'];
+
+export type Academics =
+  | 'All'
+  | 'LiberalArts'
+  | 'Law'
+  | 'SocialSciences'
+  | 'InternationalRelations'
+  | 'Literature'
+  | 'Business'
+  | 'PolicyScience'
+  | 'Psychology'
+  | 'GlobalLiberalArts'
+  | 'Film'
+  | 'InformationScience'
+  | 'ScienceAndTechnology'
+  | 'Economics'
+  | 'SportsHealthScience'
+  | 'FoodManagement'
+  | 'LifeSciences'
+  | 'Pharmacy';
+
+export class RecievedListingItemPayload {
+  @IsString()
   documentId: string; //FirebaseにおけるID
+  @IsOptional()
+  @IsDate()
   purchasedAt?: Date; //購入した日時
+  @IsOptional()
+  @IsString()
   purchasedUserId?: string; //購入したユーザーのid
+  @IsIn(allConditions)
path: nestJS/src/listing-item/interface/listing-item.payload.ts
user: login: hyuuk1
body: https://github.com/arukuArupaka/arupaka-db-manager/pull/14/commits/2ca3de91cf9fed849e9a27fd951b18a04bd487b3
created_at: 2025-03-10T09:16:03Z
author_association: COLLABORATOR
diff_hunk: @@ -35,35 +48,34 @@ export class SearchItemService {
   }
 
   async ItemCreate(createItem: RecievedListingItemPayload): Promise<string> {
path: nestJS/src/listing-item/listing-item.service.ts
user: login: hyuuk1
body: https://github.com/arukuArupaka/arupaka-db-manager/pull/14/commits/2ca3de91cf9fed849e9a27fd951b18a04bd487b3
created_at: 2025-03-10T09:16:44Z
author_association: COLLABORATOR
diff_hunk: @@ -1,15 +1,88 @@
+import { plainToInstance } from 'class-transformer';
 import {
   ArrayNotEmpty,
   ArrayUnique,
   IsArray,
   IsDate,
   IsFQDN,
+  IsIn,
   IsInt,
   IsOptional,
   IsString,
   Min,
+  validate
 } from 'class-validator';
 
+export async function validateInput<T extends object>(
+  cls: new () => T,
+  plain: object
+): Promise<T> {
+  const instance = plainToInstance(cls, plain);
+
+  const errors = await validate(instance);
+  if (errors.length > 0) {
+    throw new Error(errors.toString());
+  }
+
+  return instance;
+}
path: nestJS/src/listing-item/interface/listing-item.payload.ts
user: login: hyuuk1
body: https://github.com/arukuArupaka/arupaka-db-manager/pull/14/commits/2ca3de91cf9fed849e9a27fd951b18a04bd487b3
created_at: 2025-03-10T09:17:12Z
author_association: COLLABORATOR

PR comments:
user: login: hyuuk1
created_at: 2025-03-07T10:42:55Z
body: ### listing-item.payload
パクってきたvalidateInputを導入、
ConditionとDepartmentの値を検証するデコレータの追加。
単純な内容の型宣言を前半に移動

### service
validateInputを導入、エラーを拾ってstringでcontrollerに返るように変更

### controller
それに合わせて呼び出し元の型注釈を微変更
author_association: COLLABORATOR
user: login: sho-ritz
created_at: 2025-03-08T20:08:13Z
body: できればコンフリクトも修正してほしいんだけど、このgithubサイト上で修正はやめた方がいい。
ローカルでmainにリベースかけて子のブランチにフォースpushするのがいいと思うよ。
author_association: COLLABORATOR
user: login: sho-ritz
created_at: 2025-03-09T12:30:57Z
body: てかやっぱりサービス内で`validate`する必要はなさそうだったわ
`app.controller.ts`で`validatePipe`を設定してるから、グローバルでDTOが適応されて、全てのエンドポイントにおいて、controllerにリクエストが到達する前にバリデーションをかけてくれる
まあ普通に考えてDTOってそういうものだよね
自分もあんま調べてなかったのが悪かったわ
https://area-b.com/blog/2018/09/13/2245/
この記事で軽く解説してある
結論としては、`validate-input.ts`は不要
ただ、payloadはvalidatePipeの範囲外らしいから、returnで返す値のDTOはvalidateする必要がありそう
author_association: COLLABORATOR
user: login: sho-ritz
created_at: 2025-03-10T11:08:55Z
body: LGTM👍👍👍
author_association: COLLABORATOR