diff --git a/2024-summer-FE-seminar/.docker/docker-compose.dev.yml b/2024-summer-FE-seminar/.docker/docker-compose.dev.yml index 9e0c66b..fe95e90 100644 --- a/2024-summer-FE-seminar/.docker/docker-compose.dev.yml +++ b/2024-summer-FE-seminar/.docker/docker-compose.dev.yml @@ -3,7 +3,7 @@ version: "3" services: db: image: mysql:8.0 - container_name: sparcs-clubs-dev-db + container_name: sparcs-clubs-seminar-db ports: - "${DB_PORT:?err}:3306" environment: diff --git a/2024-summer-FE-seminar/packages/api/package.json b/2024-summer-FE-seminar/packages/api/package.json index ba672cc..c7ef733 100644 --- a/2024-summer-FE-seminar/packages/api/package.json +++ b/2024-summer-FE-seminar/packages/api/package.json @@ -8,7 +8,7 @@ "scripts": { "dev": "pnpm db up -d && pnpm start:dev", "prod": "pnpm migrate:prod && pnpm start:prod", - "db": "docker compose -f ../../.docker/docker-compose.dev.yml -p ar-002-clubs", + "db": "docker compose -f ../../.docker/docker-compose.dev.yml -p ar-002-clubs-seminar", "env": "echo $NODE_ENV", "clean": "rm -rf dist", "generate": "pnpm db up --wait && pnpm drizzle-kit push:mysql --config ./src/drizzle/drizzle.config.ts", diff --git a/2024-summer-FE-seminar/packages/api/src/drizzle/schema/promotional-printing.schema copy.ts b/2024-summer-FE-seminar/packages/api/src/drizzle/schema/promotional-printing.schema copy.ts deleted file mode 100644 index 64c8776..0000000 --- a/2024-summer-FE-seminar/packages/api/src/drizzle/schema/promotional-printing.schema copy.ts +++ /dev/null @@ -1,97 +0,0 @@ -import { - boolean, - datetime, - index, - int, - mysqlTable, - text, - timestamp, - varchar, -} from "drizzle-orm/mysql-core"; - -import { Club } from "./club.schema"; -import { Student } from "./user.schema"; - -export const PromotionalPrintingOrderStatusEnum = mysqlTable( - "promotional_printing_order_status_enum", - { - id: int("id").autoincrement().primaryKey(), - statusName: varchar("status_name", { length: 30 }), - createdAt: timestamp("created_at").defaultNow().notNull(), - updatedAt: timestamp("updated_at").defaultNow().onUpdateNow(), - deletedAt: timestamp("deleted_at"), - }, -); - -export const PromotionalPrintingSizeEnum = mysqlTable( - "promotional_printing_size_enum", - { - id: int("id").autoincrement().primaryKey(), - statusName: varchar("printing_size", { length: 30 }), - createdAt: timestamp("created_at").defaultNow().notNull(), - updatedAt: timestamp("updated_at").defaultNow().onUpdateNow(), - deletedAt: timestamp("deleted_at"), - }, -); - -// PromotionalPrintingOrder 테이블 정의 -export const PromotionalPrintingOrder = mysqlTable( - "promotional_printing_order", - { - id: int("id").autoincrement().primaryKey(), - clubId: int("club_id") - .notNull() - .references(() => Club.id), - studentId: int("student_id") - .notNull() - .references(() => Student.id), - studentPhoneNumber: varchar("student_phone_number", { length: 30 }), - promotionalPrintingOrderStatusEnum: int( - "promotional_printing_order_status_enum", - ).notNull(), - // .references(() => PromotionalPrintingOrderStatusEnum.id), - documentFileLink: text("document_file_link"), - isColorPrint: boolean("is_color_print").default(true).notNull(), - fitPrintSizeToPaper: boolean("fit_print_size_to_paper") - .default(true) - .notNull(), - requireMarginChopping: boolean("require_margin_chopping") - .default(false) - .notNull(), - desiredPickUpTime: datetime("desired_pick_up_time").notNull(), - pickUpAt: datetime("pick_up_at"), - createdAt: timestamp("created_at").defaultNow().notNull(), - updatedAt: timestamp("updated_at").defaultNow().onUpdateNow(), - deletedAt: timestamp("deleted_at"), - }, - table => ({ - promotionalPrintingOrderStatusEnumIdFk: index( - "pp_order_pp_order_status_enum_id_fk", - ).on(table.promotionalPrintingOrderStatusEnum), - }), -); - -export const PromotionalPrintingOrderSize = mysqlTable( - "promotional_printing_order_size", - { - id: int("id").autoincrement().primaryKey(), - promotionalPrintingOrderId: int("promotional_printing_order_id").notNull(), - // .references(() => PromotionalPrintingOrder.id), - promotionalPrintingSizeEnumId: int( - "promotional_printing_size_enum_id", - ).notNull(), - // .references(() => PromotionalPrintingSizeEnum.id), - numberOfPrints: int("number_of_prints").notNull(), - createdAt: timestamp("created_at").defaultNow().notNull(), - updatedAt: timestamp("updated_at").defaultNow().onUpdateNow(), - deletedAt: timestamp("deleted_at"), - }, - table => ({ - promotionalPrintingOrderIdFk: index("pp_order_size_pp_order_id_fk").on( - table.promotionalPrintingOrderId, - ), - promotionalPrintingSizeEnumIdFk: index( - "pp_order_size_pp_size_enum_id_fk", - ).on(table.promotionalPrintingSizeEnumId), - }), -); diff --git a/2024-summer-FE-seminar/packages/api/src/drizzle/schema/promotional-printing.schema.ts b/2024-summer-FE-seminar/packages/api/src/drizzle/schema/promotional-printing.schema.ts index 296d5c8..64c8776 100644 --- a/2024-summer-FE-seminar/packages/api/src/drizzle/schema/promotional-printing.schema.ts +++ b/2024-summer-FE-seminar/packages/api/src/drizzle/schema/promotional-printing.schema.ts @@ -12,24 +12,25 @@ import { import { Club } from "./club.schema"; import { Student } from "./user.schema"; -/** - * # HW1 - * DB 설계에 맞추어 drizzle schema를 구현해 봅시다. - * 진행 중 아무리 검색하고 고민해 보아도 해결되지 않는 점이 있다면, - * `promotional-printing.schema copy.ts` 파일을 참고하세요. - */ - export const PromotionalPrintingOrderStatusEnum = mysqlTable( "promotional_printing_order_status_enum", { - + id: int("id").autoincrement().primaryKey(), + statusName: varchar("status_name", { length: 30 }), + createdAt: timestamp("created_at").defaultNow().notNull(), + updatedAt: timestamp("updated_at").defaultNow().onUpdateNow(), + deletedAt: timestamp("deleted_at"), }, ); export const PromotionalPrintingSizeEnum = mysqlTable( "promotional_printing_size_enum", { - + id: int("id").autoincrement().primaryKey(), + statusName: varchar("printing_size", { length: 30 }), + createdAt: timestamp("created_at").defaultNow().notNull(), + updatedAt: timestamp("updated_at").defaultNow().onUpdateNow(), + deletedAt: timestamp("deleted_at"), }, ); @@ -37,19 +38,60 @@ export const PromotionalPrintingSizeEnum = mysqlTable( export const PromotionalPrintingOrder = mysqlTable( "promotional_printing_order", { - + id: int("id").autoincrement().primaryKey(), + clubId: int("club_id") + .notNull() + .references(() => Club.id), + studentId: int("student_id") + .notNull() + .references(() => Student.id), + studentPhoneNumber: varchar("student_phone_number", { length: 30 }), + promotionalPrintingOrderStatusEnum: int( + "promotional_printing_order_status_enum", + ).notNull(), + // .references(() => PromotionalPrintingOrderStatusEnum.id), + documentFileLink: text("document_file_link"), + isColorPrint: boolean("is_color_print").default(true).notNull(), + fitPrintSizeToPaper: boolean("fit_print_size_to_paper") + .default(true) + .notNull(), + requireMarginChopping: boolean("require_margin_chopping") + .default(false) + .notNull(), + desiredPickUpTime: datetime("desired_pick_up_time").notNull(), + pickUpAt: datetime("pick_up_at"), + createdAt: timestamp("created_at").defaultNow().notNull(), + updatedAt: timestamp("updated_at").defaultNow().onUpdateNow(), + deletedAt: timestamp("deleted_at"), }, table => ({ - + promotionalPrintingOrderStatusEnumIdFk: index( + "pp_order_pp_order_status_enum_id_fk", + ).on(table.promotionalPrintingOrderStatusEnum), }), ); export const PromotionalPrintingOrderSize = mysqlTable( "promotional_printing_order_size", { - + id: int("id").autoincrement().primaryKey(), + promotionalPrintingOrderId: int("promotional_printing_order_id").notNull(), + // .references(() => PromotionalPrintingOrder.id), + promotionalPrintingSizeEnumId: int( + "promotional_printing_size_enum_id", + ).notNull(), + // .references(() => PromotionalPrintingSizeEnum.id), + numberOfPrints: int("number_of_prints").notNull(), + createdAt: timestamp("created_at").defaultNow().notNull(), + updatedAt: timestamp("updated_at").defaultNow().onUpdateNow(), + deletedAt: timestamp("deleted_at"), }, table => ({ - + promotionalPrintingOrderIdFk: index("pp_order_size_pp_order_id_fk").on( + table.promotionalPrintingOrderId, + ), + promotionalPrintingSizeEnumIdFk: index( + "pp_order_size_pp_size_enum_id_fk", + ).on(table.promotionalPrintingSizeEnumId), }), ); diff --git a/2024-summer-FE-seminar/packages/api/src/feature/promotional-printing/repository/promotional-printing-order.repository copy.ts b/2024-summer-FE-seminar/packages/api/src/feature/promotional-printing/repository/promotional-printing-order.repository copy.ts deleted file mode 100644 index 1a93b0a..0000000 --- a/2024-summer-FE-seminar/packages/api/src/feature/promotional-printing/repository/promotional-printing-order.repository copy.ts +++ /dev/null @@ -1,202 +0,0 @@ -import { Inject, Injectable } from "@nestjs/common"; -import { PromotionalPrintingOrderStatusEnum as Status } from "@sparcs-clubs/interface/common/enum/promotionalPrinting.enum"; -import { and, count, desc, eq, gte, lte } from "drizzle-orm"; -import { MySql2Database } from "drizzle-orm/mysql2"; - -import logger from "@sparcs-clubs/api/common/util/logger"; -import { Student } from "@sparcs-clubs/api/drizzle/schema/user.schema"; -import { DrizzleAsyncProvider } from "src/drizzle/drizzle.provider"; -import { - PromotionalPrintingOrder, - PromotionalPrintingOrderSize, -} from "src/drizzle/schema/promotional-printing.schema"; - -import type { - GetStudentPromotionalPrintingsOrdersMyReturn, - GetStudentPromotionalPrintingsOrdersReturn, - PostStudentPromotionalPrintingsOrderParam, -} from "../dto/promotional-printing.dto"; - -@Injectable() -export class PromotionalPrintingOrderRepository { - constructor(@Inject(DrizzleAsyncProvider) private db: MySql2Database) {} - - async countByStudentIdAndCreatedAtIn( - studentId: number, - startDate?: Date, - endDate?: Date, - ): Promise { - const numberOfOrders = ( - await this.db - .select({ count: count() }) - .from(PromotionalPrintingOrder) - .where( - and( - eq(PromotionalPrintingOrder.studentId, studentId), - startDate !== undefined - ? gte(PromotionalPrintingOrder.createdAt, startDate) - : undefined, - endDate !== undefined - ? lte(PromotionalPrintingOrder.createdAt, endDate) - : undefined, - ), - ) - ).at(0).count; - - return numberOfOrders; - } - - async countByCreatedAtIn(startDate?: Date, endDate?: Date): Promise { - const numberOfOrders = ( - await this.db - .select({ count: count() }) - .from(PromotionalPrintingOrder) - .where( - and( - startDate !== undefined - ? gte(PromotionalPrintingOrder.createdAt, startDate) - : undefined, - endDate !== undefined - ? lte(PromotionalPrintingOrder.createdAt, endDate) - : undefined, - ), - ) - ).at(0).count; - - return numberOfOrders; - } - - async findByOrderId(orderId: number) { - const orders = await this.db - .select() - .from(PromotionalPrintingOrder) - .where(eq(PromotionalPrintingOrder.id, orderId)); - - return orders; - } - - async getStudentPromotionalPrintingsOrders( - clubId: number, - pageOffset: number, - itemCount: number, - startDate?: Date, - endDate?: Date, - ): Promise { - const startIndex = (pageOffset - 1) * itemCount + 1; - const orders = await this.db - .select({ - id: PromotionalPrintingOrder.id, - studentName: Student.name, - status: PromotionalPrintingOrder.promotionalPrintingOrderStatusEnum, - desiredPickUpDate: PromotionalPrintingOrder.desiredPickUpTime, - pickUpTime: PromotionalPrintingOrder.pickUpAt, - createdAt: PromotionalPrintingOrder.createdAt, - }) - .from(PromotionalPrintingOrder) - .leftJoin(Student, eq(PromotionalPrintingOrder.studentId, Student.id)) - .where( - and( - eq(PromotionalPrintingOrder.clubId, clubId), - startDate !== undefined - ? gte(PromotionalPrintingOrder.createdAt, startDate) - : undefined, - endDate !== undefined - ? lte(PromotionalPrintingOrder.createdAt, endDate) - : undefined, - ), - ) - .orderBy(desc(PromotionalPrintingOrder.createdAt)) - .limit(itemCount) - .offset(startIndex - 1); - - return orders; - } - - async postStudentPromotionalPrintingsOrder( - parameter: PostStudentPromotionalPrintingsOrderParam, - ) { - // 트랜잭션에 실패했을 경우의 에러 핸들링을 어떻게 하는것이 좋을까요? - await this.db.transaction(async tx => { - const [orderInsertResult] = await tx - .insert(PromotionalPrintingOrder) - .values({ - clubId: parameter.clubId, - // 아직 인증 구현이 안되어서 임의의값을 집어넣은 상태입니다(하승종 Id) - studentId: 605, - studentPhoneNumber: parameter.krPhoneNumber, - promotionalPrintingOrderStatusEnum: Status.Applied, - documentFileLink: parameter.documentFileLink, - isColorPrint: parameter.isColorPrint, - fitPrintSizeToPaper: parameter.fitPrintSizeToPaper, - requireMarginChopping: parameter.requireMarginChopping, - desiredPickUpTime: parameter.desiredPickUpTime, - }); - if (orderInsertResult.affectedRows !== 1) { - logger.debug("[postStudentPromotionalPrintingsOrder] rollback occurs"); - tx.rollback(); - } - - logger.debug( - `[postStudentPromotionalPrintingsOrder] PromotionalPrintingOrder inserted with id ${orderInsertResult.insertId}`, - ); - - parameter.orders.forEach(async order => { - const [sizeInsertResult] = await tx - .insert(PromotionalPrintingOrderSize) - .values({ - promotionalPrintingOrderId: orderInsertResult.insertId, - promotionalPrintingSizeEnumId: order.promotionalPrintingSizeEnum, - numberOfPrints: order.numberOfPrints, - }); - if (sizeInsertResult.affectedRows !== 1) { - logger.debug( - "[postStudentPromotionalPrintingsOrder] rollback occurs", - ); - tx.rollback(); - } - }); - }); - logger.debug( - "[postStudentPromotionalPrintingsOrder] insertion ends successfully", - ); - - return {}; - } - - async getStudentPromotionalPrintingsOrdersMy( - studentId: number, - pageOffset: number, - itemCount: number, - startDate?: Date, - endDate?: Date, - ): Promise { - const startIndex = (pageOffset - 1) * itemCount + 1; - const orders = await this.db - .select({ - id: PromotionalPrintingOrder.id, - studentName: Student.name, - status: PromotionalPrintingOrder.promotionalPrintingOrderStatusEnum, - desiredPickUpDate: PromotionalPrintingOrder.desiredPickUpTime, - pickUpTime: PromotionalPrintingOrder.pickUpAt, - createdAt: PromotionalPrintingOrder.createdAt, - }) - .from(PromotionalPrintingOrder) - .leftJoin(Student, eq(PromotionalPrintingOrder.studentId, Student.id)) - .where( - and( - eq(PromotionalPrintingOrder.studentId, studentId), - startDate !== undefined - ? gte(PromotionalPrintingOrder.createdAt, startDate) - : undefined, - endDate !== undefined - ? lte(PromotionalPrintingOrder.createdAt, endDate) - : undefined, - ), - ) - .orderBy(desc(PromotionalPrintingOrder.createdAt)) - .limit(itemCount) - .offset(startIndex - 1); - - return orders; - } -} diff --git a/2024-summer-FE-seminar/packages/api/src/feature/promotional-printing/repository/promotional-printing-order.repository.ts b/2024-summer-FE-seminar/packages/api/src/feature/promotional-printing/repository/promotional-printing-order.repository.ts index 5d98948..1a93b0a 100644 --- a/2024-summer-FE-seminar/packages/api/src/feature/promotional-printing/repository/promotional-printing-order.repository.ts +++ b/2024-summer-FE-seminar/packages/api/src/feature/promotional-printing/repository/promotional-printing-order.repository.ts @@ -115,13 +115,22 @@ export class PromotionalPrintingOrderRepository { async postStudentPromotionalPrintingsOrder( parameter: PostStudentPromotionalPrintingsOrderParam, ) { - // HW2: 설계한 DB 스키마에 적절하 값을 집어넣을 수 있는 쿼리를 구현하세요. + // 트랜잭션에 실패했을 경우의 에러 핸들링을 어떻게 하는것이 좋을까요? await this.db.transaction(async tx => { - // HW2-1: PromotionalPrintingOrder 스키마에 insert를 수행해야 합니다. - // studentId는 임의의 값인 studentId = 1을 이용하세요. - // 여러분의 로컬 DB는 완전히 비어있는 상태이기 때문에, user table과 student, 그리고 club 테이블에 1개씩 테스트용 값을 집어넣어야 합니다. const [orderInsertResult] = await tx - .insert(?????????); + .insert(PromotionalPrintingOrder) + .values({ + clubId: parameter.clubId, + // 아직 인증 구현이 안되어서 임의의값을 집어넣은 상태입니다(하승종 Id) + studentId: 605, + studentPhoneNumber: parameter.krPhoneNumber, + promotionalPrintingOrderStatusEnum: Status.Applied, + documentFileLink: parameter.documentFileLink, + isColorPrint: parameter.isColorPrint, + fitPrintSizeToPaper: parameter.fitPrintSizeToPaper, + requireMarginChopping: parameter.requireMarginChopping, + desiredPickUpTime: parameter.desiredPickUpTime, + }); if (orderInsertResult.affectedRows !== 1) { logger.debug("[postStudentPromotionalPrintingsOrder] rollback occurs"); tx.rollback(); @@ -131,12 +140,14 @@ export class PromotionalPrintingOrderRepository { `[postStudentPromotionalPrintingsOrder] PromotionalPrintingOrder inserted with id ${orderInsertResult.insertId}`, ); - // HW2-2: PromotionalPrintingOrderSize 스키마에 insert를 수행해야 합니다. - // 아래 skeleton 코드는 foreach를 통해 여러번의 insertion을 수행해야 합니다. - // foreach를 통해 insert하는것은 좋은 선택은 아닙니다. 반복 횟수가 매우 적어 대충 짠 코드이니 다른데서 활용하지 말아주세요...ㅎ parameter.orders.forEach(async order => { const [sizeInsertResult] = await tx - .insert(?????????); + .insert(PromotionalPrintingOrderSize) + .values({ + promotionalPrintingOrderId: orderInsertResult.insertId, + promotionalPrintingSizeEnumId: order.promotionalPrintingSizeEnum, + numberOfPrints: order.numberOfPrints, + }); if (sizeInsertResult.affectedRows !== 1) { logger.debug( "[postStudentPromotionalPrintingsOrder] rollback occurs", @@ -159,10 +170,32 @@ export class PromotionalPrintingOrderRepository { startDate?: Date, endDate?: Date, ): Promise { - // HW3: GetStudentPromotionalPrintingsOrdersMyReturn 타입에 알맞게 페이지네이션을 구현하세요. - // hint: PromotionalPrintingOrder 스키마와 Student 스키마를 studentId를 통해 join하여 정보를 select해와야 합니다. - const offset = (pageOffset - 1) * itemCount; - const orders = // ??? + const startIndex = (pageOffset - 1) * itemCount + 1; + const orders = await this.db + .select({ + id: PromotionalPrintingOrder.id, + studentName: Student.name, + status: PromotionalPrintingOrder.promotionalPrintingOrderStatusEnum, + desiredPickUpDate: PromotionalPrintingOrder.desiredPickUpTime, + pickUpTime: PromotionalPrintingOrder.pickUpAt, + createdAt: PromotionalPrintingOrder.createdAt, + }) + .from(PromotionalPrintingOrder) + .leftJoin(Student, eq(PromotionalPrintingOrder.studentId, Student.id)) + .where( + and( + eq(PromotionalPrintingOrder.studentId, studentId), + startDate !== undefined + ? gte(PromotionalPrintingOrder.createdAt, startDate) + : undefined, + endDate !== undefined + ? lte(PromotionalPrintingOrder.createdAt, endDate) + : undefined, + ), + ) + .orderBy(desc(PromotionalPrintingOrder.createdAt)) + .limit(itemCount) + .offset(startIndex - 1); return orders; } diff --git a/2024-summer-FE-seminar/packages/web/src/app/casio/page.tsx b/2024-summer-FE-seminar/packages/web/src/app/casio/page.tsx new file mode 100644 index 0000000..64949ba --- /dev/null +++ b/2024-summer-FE-seminar/packages/web/src/app/casio/page.tsx @@ -0,0 +1,51 @@ +"use client" + +import React, {useState} from "react" + +import TextInput from "@sparcs-clubs/web/common/components/casio/TextInput"; +import ItemNumberInput from "@sparcs-clubs/web/common/components/Forms/ItemNumberInput"; + +const Casio : React.FC = () => { + const [ textValue, setTextValue ] = useState(""); + + const [numberValue, setNumberValue] = useState(""); + const [errorMessage, setErrorMessage] = useState(""); + const [hasError, setHasError] = useState(false); + + const handleNumberChange = (value: string) => { + setNumberValue(value); + const numberValue = parseInt(value.replace(/개$/, ""), 10); + if (isNaN(numberValue)) { + setErrorMessage("입력은 숫자여야 합니다"); + setHasError(true); + } else if (numberValue > 99) { + setErrorMessage("입력은 99보다 작아야합니다."); + setHasError(true); + } else { + setErrorMessage(""); + setHasError(false); + } + }; + + return( +
+ {setTextValue(value);}} + > + +
+ ); +} + + +export default Casio; \ No newline at end of file diff --git a/2024-summer-FE-seminar/packages/web/src/app/eel/page.tsx b/2024-summer-FE-seminar/packages/web/src/app/eel/page.tsx new file mode 100644 index 0000000..6cbe7a0 --- /dev/null +++ b/2024-summer-FE-seminar/packages/web/src/app/eel/page.tsx @@ -0,0 +1,17 @@ +"use client" + +import React from "react" + +import ItemNumberInput from "@sparcs-clubs/web/common/components/Forms/ItemNumberInput"; +import TextInput from "@sparcs-clubs/web/common/components/Forms/TextInput"; +import Select from "@sparcs-clubs/web/common/components/Select"; + +const Eel = () => ( +
+ + + +
+); + +export default Eel; diff --git a/2024-summer-FE-seminar/packages/web/src/common/components/casio/ItemNumberInput.tsx b/2024-summer-FE-seminar/packages/web/src/common/components/casio/ItemNumberInput.tsx new file mode 100644 index 0000000..2518fae --- /dev/null +++ b/2024-summer-FE-seminar/packages/web/src/common/components/casio/ItemNumberInput.tsx @@ -0,0 +1,100 @@ +import React, { ChangeEvent, InputHTMLAttributes, useEffect } from "react"; +import styled, { css } from "styled-components"; +import FormError from "../FormError"; +import Label from "../FormLabel"; + +interface ItemNumberInputProps extends InputHTMLAttributes { + label?: string; + placeholder: string; + disabled: boolean; + value: string; + errorMessage?: string; + handleChange: (value: string) => void; + setErrorStatus: (hasError: boolean) => void; +} + +const errorBorderStyle = css` + border-color: ${({ theme }) => theme.colors.RED[600]}; +`; + +const disabledStyle = css` + background-color: ${({ theme }) => theme.colors.GRAY[100]}; + border-color: ${({ theme }) => theme.colors.GRAY[200]}; +`; + +const Input = styled.input<{ hasError: boolean }>` + display: block; + width: 100%; + padding: 8px 12px; + outline: none; + border: 1px solid ${({ theme }) => theme.colors.GRAY[200]}; + border-radius: 4px; + gap: 8px; + font-family: ${({ theme }) => theme.fonts.FAMILY.PRETENDARD}; + font-size: 16px; + line-height: 20px; + font-weight: ${({ theme }) => theme.fonts.WEIGHT.REGULAR}; + color: ${({ theme }) => theme.colors.BLACK}; + background-color: ${({ theme }) => theme.colors.WHITE}; + + &:focus { + border-color: ${({ theme, hasError, disabled }) => + !hasError && !disabled && theme.colors.PRIMARY}; + } + &:hover:not(:focus) { + border-color: ${({ theme, hasError, disabled }) => + !hasError && !disabled && theme.colors.GRAY[300]}; + } + &::placeholder { + color: ${({ theme }) => theme.colors.GRAY[200]}; + } + ${({ disabled }) => disabled && disabledStyle} + ${({ hasError }) => hasError && errorBorderStyle} +`; + +const InputWrapper = styled.div` + width: 100%; + flex-direction: column; + display: flex; + gap: 4px; +`; + +const ItemNumberInput: React.FC = ({ + label = "", + placeholder, + errorMessage = "", + disabled = false, + value = "", + handleChange = () => {}, + setErrorStatus = () => {}, + ...props +}) => { + const handleValueChange = (e: ChangeEvent) => { + const inputValue = e.target.value; + handleChange(inputValue); + }; + + useEffect(() => { + const hasError = !!errorMessage; + setErrorStatus(hasError); + }, [errorMessage, setErrorStatus]); + + return ( + + {label && } + + + {errorMessage && {errorMessage}} + + + ); +}; + +export default ItemNumberInput; diff --git a/2024-summer-FE-seminar/packages/web/src/common/components/casio/TextInput.tsx b/2024-summer-FE-seminar/packages/web/src/common/components/casio/TextInput.tsx new file mode 100644 index 0000000..a91fe63 --- /dev/null +++ b/2024-summer-FE-seminar/packages/web/src/common/components/casio/TextInput.tsx @@ -0,0 +1,104 @@ +import React, { ChangeEvent, InputHTMLAttributes, useEffect } from "react"; +import styled, { css } from "styled-components"; +import FormError from "../FormError"; +import Label from "../FormLabel"; + +interface TextInputProps extends InputHTMLAttributes { + label?: string; + placeholder?: string; + disabled?: boolean; + value?: string; + errorMessage?: string; + handleChange?: (value: string) => void; + setErrorStatus?: (hasError: boolean) => void; +} + +const errorBorderStyle = css` + border-color: ${({ theme }) => theme.colors.RED[600]}; +`; + +const disabledStyle = css` + background-color: ${({ theme }) => theme.colors.GRAY[100]}; + border-color: ${({ theme }) => theme.colors.GRAY[200]}; +`; + +interface StyledInputProps { + hasError: boolean; +} + +const Input = styled.input` + display: block; + width: 100%; + padding: 8px 12px; + outline: none; + border: 1px solid ${({ theme }) => theme.colors.GRAY[200]}; + border-radius: 4px; + gap: 8px; + font-family: ${({ theme }) => theme.fonts.FAMILY.PRETENDARD}; + font-size: 16px; + line-height: 20px; + font-weight: ${({ theme }) => theme.fonts.WEIGHT.REGULAR}; + color: ${({ theme }) => theme.colors.BLACK}; + background-color: ${({ theme }) => theme.colors.WHITE}; + + &:focus { + border-color: ${({ theme, hasError, disabled }) => + !hasError && !disabled && theme.colors.PRIMARY}; + } + &:hover:not(:focus) { + border-color: ${({ theme, hasError, disabled }) => + !hasError && !disabled && theme.colors.GRAY[300]}; + } + &::placeholder { + color: ${({ theme }) => theme.colors.GRAY[200]}; + } + ${({ disabled }) => disabled && disabledStyle} + ${({ hasError }) => hasError && errorBorderStyle} +`; + +const InputWrapper = styled.div` + width: 100%; + flex-direction: column; + display: flex; + gap: 4px; +`; + +const TextInput: React.FC = ({ + label = "", + placeholder, + errorMessage = "", + disabled = false, + value = "", + handleChange = () => {}, + setErrorStatus = () => {}, + ...props +}) => { + const handleValueChange = (e: ChangeEvent) => { + const inputValue = e.target.value; + handleChange(inputValue); + }; + + useEffect(() => { + const hasError = !!errorMessage; + setErrorStatus(hasError); + }, [errorMessage, setErrorStatus]); + + return ( + + {label && } + + )} // Ensure props are of type InputHTMLAttributes + /> + {errorMessage && {errorMessage}} + + + ); +}; + +export default TextInput; diff --git a/2024-summer-zod-semina/.gitignore b/2024-summer-zod-semina/.gitignore index ed4598e..ea95b17 100644 --- a/2024-summer-zod-semina/.gitignore +++ b/2024-summer-zod-semina/.gitignore @@ -1,2 +1,3 @@ node_modules bin +pnpm-lock.yaml \ No newline at end of file diff --git a/2024-summer-zod-semina/interface/db.ts b/2024-summer-zod-semina/interface/db.ts index c0c6f6e..2888d1f 100644 --- a/2024-summer-zod-semina/interface/db.ts +++ b/2024-summer-zod-semina/interface/db.ts @@ -3,17 +3,22 @@ import { z } from "zod"; export const dbElement = z.object({ // TODO 2: mockDB에 들어갈 데이터의 타입을 정의해 주세요 // { value: 0 이샹의 정수, updatedAt: ts date 타입 } + value : z.coerce.number().int().min(0), + updatedAt : z.coerce.date(), }); export const twoPositiveIntegerParser = z.object({ // TODO 1: 아래와 같이 2개의 양의 정수를 포함한 객체를 parsing하는 zod object를 구현하세요 // { a: 양의 정수, b: 양의 정수 } + a : z.coerce.number().int().positive(), + b : z.coerce.number().int().positive(), }); export const getDbIndexParser = (dbLength: number) => z.object({ // db의 크기를 받아 db 범위 내의 인덱스만을 값으로 받는 parser를 만들어 주세요 // { index: int, 0 <= index <= db_max_index} + index : z.coerce.number().int().min(0).max(dbLength - 1), }); export const dbPaginationQueryParser = z.object({ @@ -22,4 +27,8 @@ export const dbPaginationQueryParser = z.object({ // pageOffset: int, >= 1 // itemCount: int, >= 1 // }, + startDate : z.optional(z.coerce.date()), + endDate : z.optional(z.coerce.date()), + pageOffset : z.coerce.number().int().min(1), + itemCount : z.coerce.number().int().min(1), });