diff --git a/.env b/.env
new file mode 100644
index 000000000..de28370b9
--- /dev/null
+++ b/.env
@@ -0,0 +1 @@
+VITE_BASE_URL="/"
\ No newline at end of file
diff --git a/.eslintrc.cjs b/.eslintrc.cjs
new file mode 100644
index 000000000..29cb6d5a0
--- /dev/null
+++ b/.eslintrc.cjs
@@ -0,0 +1,14 @@
+module.exports = {
+ root: true,
+ env: { browser: true, es2020: true },
+ extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended', 'plugin:react-hooks/recommended', 'plugin:storybook/recommended'],
+ ignorePatterns: ['dist', '.eslintrc.cjs'],
+ parser: '@typescript-eslint/parser',
+ plugins: ['react-refresh'],
+ rules: {
+ 'react-refresh/only-export-components': [
+ 'warn',
+ { allowConstantExport: true },
+ ],
+ },
+}
diff --git a/.gitignore b/.gitignore
index 4642caf14..3f6516936 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,151 +1,28 @@
-# Created by https://www.toptal.com/developers/gitignore/api/macos,windows,jetbrains+all
-# Edit at https://www.toptal.com/developers/gitignore?templates=macos,windows,jetbrains+all
-
-### JetBrains+all ###
-# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
-# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
-
-# User-specific stuff
-.idea/**/workspace.xml
-.idea/**/tasks.xml
-.idea/**/usage.statistics.xml
-.idea/**/dictionaries
-.idea/**/shelf
-
-# AWS User-specific
-.idea/**/aws.xml
-
-# Generated files
-.idea/**/contentModel.xml
-
-# Sensitive or high-churn files
-.idea/**/dataSources/
-.idea/**/dataSources.ids
-.idea/**/dataSources.local.xml
-.idea/**/sqlDataSources.xml
-.idea/**/dynamic.xml
-.idea/**/uiDesigner.xml
-.idea/**/dbnavigator.xml
-
-# Gradle
-.idea/**/gradle.xml
-.idea/**/libraries
-
-# Gradle and Maven with auto-import
-# When using Gradle or Maven with auto-import, you should exclude module files,
-# since they will be recreated, and may cause churn. Uncomment if using
-# auto-import.
-# .idea/artifacts
-# .idea/compiler.xml
-# .idea/jarRepositories.xml
-# .idea/modules.xml
-# .idea/*.iml
-# .idea/modules
-# *.iml
-# *.ipr
-
-# CMake
-cmake-build-*/
-
-# Mongo Explorer plugin
-.idea/**/mongoSettings.xml
-
-# File-based project format
-*.iws
-
-# IntelliJ
-out/
-
-# mpeltonen/sbt-idea plugin
-.idea_modules/
-
-# JIRA plugin
-atlassian-ide-plugin.xml
-
-# Cursive Clojure plugin
-.idea/replstate.xml
-
-# SonarLint plugin
-.idea/sonarlint/
-
-# Crashlytics plugin (for Android Studio and IntelliJ)
-com_crashlytics_export_strings.xml
-crashlytics.properties
-crashlytics-build.properties
-fabric.properties
-
-# Editor-based Rest Client
-.idea/httpRequests
-
-# Android studio 3.1+ serialized cache file
-.idea/caches/build_file_checksums.ser
-
-### JetBrains+all Patch ###
-# Ignore everything but code style settings and run configurations
-# that are supposed to be shared within teams.
-
-.idea/*
-
-!.idea/codeStyles
-!.idea/runConfigurations
-
-### macOS ###
-# General
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+dist
+dist-ssr
+*.local
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
.DS_Store
-.AppleDouble
-.LSOverride
-
-# Icon must end with two \r
-Icon
-
-
-# Thumbnails
-._*
-
-# Files that might appear in the root of a volume
-.DocumentRevisions-V100
-.fseventsd
-.Spotlight-V100
-.TemporaryItems
-.Trashes
-.VolumeIcon.icns
-.com.apple.timemachine.donotpresent
-
-# Directories potentially created on remote AFP share
-.AppleDB
-.AppleDesktop
-Network Trash Folder
-Temporary Items
-.apdisk
-
-### macOS Patch ###
-# iCloud generated files
-*.icloud
-
-### Windows ###
-# Windows thumbnail cache files
-Thumbs.db
-Thumbs.db:encryptable
-ehthumbs.db
-ehthumbs_vista.db
-
-# Dump file
-*.stackdump
-
-# Folder config file
-[Dd]esktop.ini
-
-# Recycle Bin used on file shares
-$RECYCLE.BIN/
-
-# Windows Installer files
-*.cab
-*.msi
-*.msix
-*.msm
-*.msp
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
-# Windows shortcuts
-*.lnk
+*storybook.log
-# End of https://www.toptal.com/developers/gitignore/api/macos,windows,jetbrains+all
\ No newline at end of file
+.yarn
\ No newline at end of file
diff --git a/.storybook/main.ts b/.storybook/main.ts
new file mode 100644
index 000000000..69ac0074d
--- /dev/null
+++ b/.storybook/main.ts
@@ -0,0 +1,20 @@
+import type { StorybookConfig } from "@storybook/react-vite";
+
+const config: StorybookConfig = {
+ stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|mjs|ts|tsx)"],
+ addons: [
+ "@storybook/addon-onboarding",
+ "@storybook/addon-links",
+ "@storybook/addon-essentials",
+ "@chromatic-com/storybook",
+ "@storybook/addon-interactions",
+ ],
+ framework: {
+ name: "@storybook/react-vite",
+ options: {},
+ },
+ docs: {
+ autodocs: "tag",
+ },
+};
+export default config;
diff --git a/.storybook/preview.ts b/.storybook/preview.ts
new file mode 100644
index 000000000..37914b18f
--- /dev/null
+++ b/.storybook/preview.ts
@@ -0,0 +1,14 @@
+import type { Preview } from "@storybook/react";
+
+const preview: Preview = {
+ parameters: {
+ controls: {
+ matchers: {
+ color: /(background|color)$/i,
+ date: /Date$/i,
+ },
+ },
+ },
+};
+
+export default preview;
diff --git a/README.md b/README.md
index ff3d41dc6..c5deab255 100644
--- a/README.md
+++ b/README.md
@@ -37,20 +37,20 @@
```json
{
- "response": [
- {
- "id": 1,
- "price": 10000,
- "name": "치킨",
- "imageUrl": "http://example.com/chicken.jpg"
- },
- {
- "id": 2,
- "price": 20000,
- "name": "피자",
- "imageUrl": "http://example.com/pizza.jpg"
- }
- ]
+ "response": [
+ {
+ "id": 1,
+ "price": 10000,
+ "name": "치킨",
+ "imageUrl": "http://example.com/chicken.jpg"
+ },
+ {
+ "id": 2,
+ "price": 20000,
+ "name": "피자",
+ "imageUrl": "http://example.com/pizza.jpg"
+ }
+ ]
}
```
@@ -62,13 +62,13 @@
```json
{
- "requestBody": {
- "products": {
- "price": 10000,
- "name": "치킨",
- "imageUrl": "http://example.com/chicken.jpg"
- }
- }
+ "requestBody": {
+ "products": {
+ "price": 10000,
+ "name": "치킨",
+ "imageUrl": "http://example.com/chicken.jpg"
+ }
+ }
}
```
@@ -80,12 +80,12 @@
```json
{
- "response": {
- "id": 1,
- "price": 10000,
- "name": "치킨",
- "imageUrl": "http://example.com/chicken.jpg"
- }
+ "response": {
+ "id": 1,
+ "price": 10000,
+ "name": "치킨",
+ "imageUrl": "http://example.com/chicken.jpg"
+ }
}
```
@@ -97,7 +97,7 @@
```json
{
- "response": {}
+ "response": {}
}
```
@@ -140,14 +140,14 @@
```json
{
- "requestBody": {
- "product": {
- "id": 10,
- "name": "tes11111t",
- "price": 1234,
- "imageUrl": "test.com"
- }
- }
+ "requestBody": {
+ "product": {
+ "id": 10,
+ "name": "tes11111t",
+ "price": 1234,
+ "imageUrl": "test.com"
+ }
+ }
}
```
@@ -159,7 +159,7 @@
```json
{
- "response": {}
+ "response": {}
}
```
@@ -173,24 +173,24 @@
```json
{
- "requestBody": {
- "orderDetails": [
- {
- "id": 1,
- "price": 10000,
- "name": "치킨",
- "imageUrl": "http://example.com/chicken.jpg",
- "quantity": 5
- },
- {
- "id": 2,
- "price": 20000,
- "name": "피자",
- "imageUrl": "http://example.com/pizza.jpg",
- "quantity": 3
- }
- ]
- }
+ "requestBody": {
+ "orderDetails": [
+ {
+ "id": 1,
+ "price": 10000,
+ "name": "치킨",
+ "imageUrl": "http://example.com/chicken.jpg",
+ "quantity": 5
+ },
+ {
+ "id": 2,
+ "price": 20000,
+ "name": "피자",
+ "imageUrl": "http://example.com/pizza.jpg",
+ "quantity": 3
+ }
+ ]
+ }
}
```
@@ -202,46 +202,46 @@
```json
{
- "response": [
- {
- "id": 1,
- "orderDetails": [
- {
- "id": 1,
- "price": 10000,
- "name": "치킨",
- "imageUrl": "http://example.com/chicken.jpg",
- "quantity": 5
- },
- {
- "id": 2,
- "price": 20000,
- "name": "피자",
- "imageUrl": "http://example.com/pizza.jpg",
- "quantity": 3
- }
- ]
- },
- {
- "id": 2,
- "orderDetails": [
- {
- "id": 1,
- "price": 10000,
- "name": "치킨",
- "imageUrl": "http://example.com/chicken.jpg",
- "quantity": 5
- },
- {
- "id": 2,
- "price": 20000,
- "name": "피자",
- "imageUrl": "http://example.com/pizza.jpg",
- "quantity": 3
- }
- ]
- }
- ]
+ "response": [
+ {
+ "id": 1,
+ "orderDetails": [
+ {
+ "id": 1,
+ "price": 10000,
+ "name": "치킨",
+ "imageUrl": "http://example.com/chicken.jpg",
+ "quantity": 5
+ },
+ {
+ "id": 2,
+ "price": 20000,
+ "name": "피자",
+ "imageUrl": "http://example.com/pizza.jpg",
+ "quantity": 3
+ }
+ ]
+ },
+ {
+ "id": 2,
+ "orderDetails": [
+ {
+ "id": 1,
+ "price": 10000,
+ "name": "치킨",
+ "imageUrl": "http://example.com/chicken.jpg",
+ "quantity": 5
+ },
+ {
+ "id": 2,
+ "price": 20000,
+ "name": "피자",
+ "imageUrl": "http://example.com/pizza.jpg",
+ "quantity": 3
+ }
+ ]
+ }
+ ]
}
```
@@ -253,24 +253,24 @@
```json
{
- "response": {
- "id": 1,
- "orderDetails": [
- {
- "id": 1,
- "price": 10000,
- "name": "치킨",
- "imageUrl": "http://example.com/chicken.jpg",
- "quantity": 5
- },
- {
- "id": 2,
- "price": 20000,
- "name": "피자",
- "imageUrl": "http://example.com/pizza.jpg",
- "quantity": 3
- }
- ]
- }
+ "response": {
+ "id": 1,
+ "orderDetails": [
+ {
+ "id": 1,
+ "price": 10000,
+ "name": "치킨",
+ "imageUrl": "http://example.com/chicken.jpg",
+ "quantity": 5
+ },
+ {
+ "id": 2,
+ "price": 20000,
+ "name": "피자",
+ "imageUrl": "http://example.com/pizza.jpg",
+ "quantity": 3
+ }
+ ]
+ }
}
```
diff --git a/index.html b/index.html
new file mode 100644
index 000000000..e4b78eae1
--- /dev/null
+++ b/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+ Vite + React + TS
+
+
+
+
+
+
diff --git a/mockServiceWorker.js b/mockServiceWorker.js
new file mode 100644
index 000000000..3a2c243fd
--- /dev/null
+++ b/mockServiceWorker.js
@@ -0,0 +1,284 @@
+/* eslint-disable */
+/* tslint:disable */
+
+/**
+ * Mock Service Worker.
+ * @see https://github.com/mswjs/msw
+ * - Please do NOT modify this file.
+ * - Please do NOT serve this file on production.
+ */
+
+const PACKAGE_VERSION = '2.2.10'
+const INTEGRITY_CHECKSUM = '26357c79639bfa20d64c0efca2a87423'
+const IS_MOCKED_RESPONSE = Symbol('isMockedResponse')
+const activeClientIds = new Set()
+
+self.addEventListener('install', function () {
+ self.skipWaiting()
+})
+
+self.addEventListener('activate', function (event) {
+ event.waitUntil(self.clients.claim())
+})
+
+self.addEventListener('message', async function (event) {
+ const clientId = event.source.id
+
+ if (!clientId || !self.clients) {
+ return
+ }
+
+ const client = await self.clients.get(clientId)
+
+ if (!client) {
+ return
+ }
+
+ const allClients = await self.clients.matchAll({
+ type: 'window',
+ })
+
+ switch (event.data) {
+ case 'KEEPALIVE_REQUEST': {
+ sendToClient(client, {
+ type: 'KEEPALIVE_RESPONSE',
+ })
+ break
+ }
+
+ case 'INTEGRITY_CHECK_REQUEST': {
+ sendToClient(client, {
+ type: 'INTEGRITY_CHECK_RESPONSE',
+ payload: {
+ packageVersion: PACKAGE_VERSION,
+ checksum: INTEGRITY_CHECKSUM,
+ },
+ })
+ break
+ }
+
+ case 'MOCK_ACTIVATE': {
+ activeClientIds.add(clientId)
+
+ sendToClient(client, {
+ type: 'MOCKING_ENABLED',
+ payload: true,
+ })
+ break
+ }
+
+ case 'MOCK_DEACTIVATE': {
+ activeClientIds.delete(clientId)
+ break
+ }
+
+ case 'CLIENT_CLOSED': {
+ activeClientIds.delete(clientId)
+
+ const remainingClients = allClients.filter((client) => {
+ return client.id !== clientId
+ })
+
+ // Unregister itself when there are no more clients
+ if (remainingClients.length === 0) {
+ self.registration.unregister()
+ }
+
+ break
+ }
+ }
+})
+
+self.addEventListener('fetch', function (event) {
+ const { request } = event
+
+ // Bypass navigation requests.
+ if (request.mode === 'navigate') {
+ return
+ }
+
+ // Opening the DevTools triggers the "only-if-cached" request
+ // that cannot be handled by the worker. Bypass such requests.
+ if (request.cache === 'only-if-cached' && request.mode !== 'same-origin') {
+ return
+ }
+
+ // Bypass all requests when there are no active clients.
+ // Prevents the self-unregistered worked from handling requests
+ // after it's been deleted (still remains active until the next reload).
+ if (activeClientIds.size === 0) {
+ return
+ }
+
+ // Generate unique request ID.
+ const requestId = crypto.randomUUID()
+ event.respondWith(handleRequest(event, requestId))
+})
+
+async function handleRequest(event, requestId) {
+ const client = await resolveMainClient(event)
+ const response = await getResponse(event, client, requestId)
+
+ // Send back the response clone for the "response:*" life-cycle events.
+ // Ensure MSW is active and ready to handle the message, otherwise
+ // this message will pend indefinitely.
+ if (client && activeClientIds.has(client.id)) {
+ ;(async function () {
+ const responseClone = response.clone()
+
+ sendToClient(
+ client,
+ {
+ type: 'RESPONSE',
+ payload: {
+ requestId,
+ isMockedResponse: IS_MOCKED_RESPONSE in response,
+ type: responseClone.type,
+ status: responseClone.status,
+ statusText: responseClone.statusText,
+ body: responseClone.body,
+ headers: Object.fromEntries(responseClone.headers.entries()),
+ },
+ },
+ [responseClone.body],
+ )
+ })()
+ }
+
+ return response
+}
+
+// Resolve the main client for the given event.
+// Client that issues a request doesn't necessarily equal the client
+// that registered the worker. It's with the latter the worker should
+// communicate with during the response resolving phase.
+async function resolveMainClient(event) {
+ const client = await self.clients.get(event.clientId)
+
+ if (client?.frameType === 'top-level') {
+ return client
+ }
+
+ const allClients = await self.clients.matchAll({
+ type: 'window',
+ })
+
+ return allClients
+ .filter((client) => {
+ // Get only those clients that are currently visible.
+ return client.visibilityState === 'visible'
+ })
+ .find((client) => {
+ // Find the client ID that's recorded in the
+ // set of clients that have registered the worker.
+ return activeClientIds.has(client.id)
+ })
+}
+
+async function getResponse(event, client, requestId) {
+ const { request } = event
+
+ // Clone the request because it might've been already used
+ // (i.e. its body has been read and sent to the client).
+ const requestClone = request.clone()
+
+ function passthrough() {
+ const headers = Object.fromEntries(requestClone.headers.entries())
+
+ // Remove internal MSW request header so the passthrough request
+ // complies with any potential CORS preflight checks on the server.
+ // Some servers forbid unknown request headers.
+ delete headers['x-msw-intention']
+
+ return fetch(requestClone, { headers })
+ }
+
+ // Bypass mocking when the client is not active.
+ if (!client) {
+ return passthrough()
+ }
+
+ // Bypass initial page load requests (i.e. static assets).
+ // The absence of the immediate/parent client in the map of the active clients
+ // means that MSW hasn't dispatched the "MOCK_ACTIVATE" event yet
+ // and is not ready to handle requests.
+ if (!activeClientIds.has(client.id)) {
+ return passthrough()
+ }
+
+ // Notify the client that a request has been intercepted.
+ const requestBuffer = await request.arrayBuffer()
+ const clientMessage = await sendToClient(
+ client,
+ {
+ type: 'REQUEST',
+ payload: {
+ id: requestId,
+ url: request.url,
+ mode: request.mode,
+ method: request.method,
+ headers: Object.fromEntries(request.headers.entries()),
+ cache: request.cache,
+ credentials: request.credentials,
+ destination: request.destination,
+ integrity: request.integrity,
+ redirect: request.redirect,
+ referrer: request.referrer,
+ referrerPolicy: request.referrerPolicy,
+ body: requestBuffer,
+ keepalive: request.keepalive,
+ },
+ },
+ [requestBuffer],
+ )
+
+ switch (clientMessage.type) {
+ case 'MOCK_RESPONSE': {
+ return respondWithMock(clientMessage.data)
+ }
+
+ case 'PASSTHROUGH': {
+ return passthrough()
+ }
+ }
+
+ return passthrough()
+}
+
+function sendToClient(client, message, transferrables = []) {
+ return new Promise((resolve, reject) => {
+ const channel = new MessageChannel()
+
+ channel.port1.onmessage = (event) => {
+ if (event.data && event.data.error) {
+ return reject(event.data.error)
+ }
+
+ resolve(event.data)
+ }
+
+ client.postMessage(
+ message,
+ [channel.port2].concat(transferrables.filter(Boolean)),
+ )
+ })
+}
+
+async function respondWithMock(response) {
+ // Setting response status code to 0 is a no-op.
+ // However, when responding with a "Response.error()", the produced Response
+ // instance will have status code set to 0. Since it's not possible to create
+ // a Response instance with status code 0, handle that use-case separately.
+ if (response.status === 0) {
+ return Response.error()
+ }
+
+ const mockedResponse = new Response(response.body, response)
+
+ Reflect.defineProperty(mockedResponse, IS_MOCKED_RESPONSE, {
+ value: true,
+ enumerable: true,
+ })
+
+ return mockedResponse
+}
diff --git a/package.json b/package.json
index 9750859c6..b810eb19c 100644
--- a/package.json
+++ b/package.json
@@ -1,15 +1,59 @@
{
- "name": "server",
- "version": "1.0.0",
- "main": "index.js",
- "type": "commonjs",
- "license": "MIT",
+ "name": "react-shopping-cart",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "tsc && vite build",
+ "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
+ "storybook": "storybook dev -p 6006",
+ "build-storybook": "storybook build",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "@tanstack/react-query": "^5.28.6",
+ "@tanstack/react-router": "^1.22.3",
+ "@vanilla-extract/css": "^1.14.1",
+ "@vanilla-extract/recipes": "^0.5.2",
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0",
+ "zod": "^3.22.4"
+ },
"devDependencies": {
- "json-server": "^0.17.0",
- "lowdb": "^1.0.0"
+ "@chromatic-com/storybook": "^1.2.25",
+ "@storybook/addon-essentials": "^8.0.4",
+ "@storybook/addon-interactions": "^8.0.4",
+ "@storybook/addon-links": "^8.0.4",
+ "@storybook/addon-onboarding": "^8.0.4",
+ "@storybook/blocks": "^8.0.4",
+ "@storybook/react": "^8.0.4",
+ "@storybook/react-vite": "^8.0.4",
+ "@storybook/test": "^8.0.4",
+ "@tanstack/react-query-devtools": "^5.28.9",
+ "@tanstack/router-devtools": "^1.22.3",
+ "@tanstack/router-vite-plugin": "^1.20.5",
+ "@types/react": "^18.2.66",
+ "@types/react-dom": "^18.2.22",
+ "@typescript-eslint/eslint-plugin": "^7.2.0",
+ "@typescript-eslint/parser": "^7.2.0",
+ "@vanilla-extract/vite-plugin": "^4.0.6",
+ "@vitejs/plugin-react": "^4.2.1",
+ "eslint": "^8.57.0",
+ "eslint-plugin-react-hooks": "^4.6.0",
+ "eslint-plugin-react-refresh": "^0.4.6",
+ "eslint-plugin-storybook": "^0.8.0",
+ "msw": "^2.2.10",
+ "storybook": "^8.0.4",
+ "typescript": "^5.2.2",
+ "vite": "^5.2.0"
},
- "scripts": {
- "server": "node server.js"
+ "resolutions": {
+ "@types/mime": "3.0.4"
},
- "dependencies": {}
+ "msw": {
+ "workerDirectory": [
+ "public"
+ ]
+ }
}
diff --git a/public/icons/carts.svg b/public/icons/carts.svg
new file mode 100644
index 000000000..1d41a1165
--- /dev/null
+++ b/public/icons/carts.svg
@@ -0,0 +1,3 @@
+
diff --git a/public/mockServiceWorker.js b/public/mockServiceWorker.js
new file mode 100644
index 000000000..3a2c243fd
--- /dev/null
+++ b/public/mockServiceWorker.js
@@ -0,0 +1,284 @@
+/* eslint-disable */
+/* tslint:disable */
+
+/**
+ * Mock Service Worker.
+ * @see https://github.com/mswjs/msw
+ * - Please do NOT modify this file.
+ * - Please do NOT serve this file on production.
+ */
+
+const PACKAGE_VERSION = '2.2.10'
+const INTEGRITY_CHECKSUM = '26357c79639bfa20d64c0efca2a87423'
+const IS_MOCKED_RESPONSE = Symbol('isMockedResponse')
+const activeClientIds = new Set()
+
+self.addEventListener('install', function () {
+ self.skipWaiting()
+})
+
+self.addEventListener('activate', function (event) {
+ event.waitUntil(self.clients.claim())
+})
+
+self.addEventListener('message', async function (event) {
+ const clientId = event.source.id
+
+ if (!clientId || !self.clients) {
+ return
+ }
+
+ const client = await self.clients.get(clientId)
+
+ if (!client) {
+ return
+ }
+
+ const allClients = await self.clients.matchAll({
+ type: 'window',
+ })
+
+ switch (event.data) {
+ case 'KEEPALIVE_REQUEST': {
+ sendToClient(client, {
+ type: 'KEEPALIVE_RESPONSE',
+ })
+ break
+ }
+
+ case 'INTEGRITY_CHECK_REQUEST': {
+ sendToClient(client, {
+ type: 'INTEGRITY_CHECK_RESPONSE',
+ payload: {
+ packageVersion: PACKAGE_VERSION,
+ checksum: INTEGRITY_CHECKSUM,
+ },
+ })
+ break
+ }
+
+ case 'MOCK_ACTIVATE': {
+ activeClientIds.add(clientId)
+
+ sendToClient(client, {
+ type: 'MOCKING_ENABLED',
+ payload: true,
+ })
+ break
+ }
+
+ case 'MOCK_DEACTIVATE': {
+ activeClientIds.delete(clientId)
+ break
+ }
+
+ case 'CLIENT_CLOSED': {
+ activeClientIds.delete(clientId)
+
+ const remainingClients = allClients.filter((client) => {
+ return client.id !== clientId
+ })
+
+ // Unregister itself when there are no more clients
+ if (remainingClients.length === 0) {
+ self.registration.unregister()
+ }
+
+ break
+ }
+ }
+})
+
+self.addEventListener('fetch', function (event) {
+ const { request } = event
+
+ // Bypass navigation requests.
+ if (request.mode === 'navigate') {
+ return
+ }
+
+ // Opening the DevTools triggers the "only-if-cached" request
+ // that cannot be handled by the worker. Bypass such requests.
+ if (request.cache === 'only-if-cached' && request.mode !== 'same-origin') {
+ return
+ }
+
+ // Bypass all requests when there are no active clients.
+ // Prevents the self-unregistered worked from handling requests
+ // after it's been deleted (still remains active until the next reload).
+ if (activeClientIds.size === 0) {
+ return
+ }
+
+ // Generate unique request ID.
+ const requestId = crypto.randomUUID()
+ event.respondWith(handleRequest(event, requestId))
+})
+
+async function handleRequest(event, requestId) {
+ const client = await resolveMainClient(event)
+ const response = await getResponse(event, client, requestId)
+
+ // Send back the response clone for the "response:*" life-cycle events.
+ // Ensure MSW is active and ready to handle the message, otherwise
+ // this message will pend indefinitely.
+ if (client && activeClientIds.has(client.id)) {
+ ;(async function () {
+ const responseClone = response.clone()
+
+ sendToClient(
+ client,
+ {
+ type: 'RESPONSE',
+ payload: {
+ requestId,
+ isMockedResponse: IS_MOCKED_RESPONSE in response,
+ type: responseClone.type,
+ status: responseClone.status,
+ statusText: responseClone.statusText,
+ body: responseClone.body,
+ headers: Object.fromEntries(responseClone.headers.entries()),
+ },
+ },
+ [responseClone.body],
+ )
+ })()
+ }
+
+ return response
+}
+
+// Resolve the main client for the given event.
+// Client that issues a request doesn't necessarily equal the client
+// that registered the worker. It's with the latter the worker should
+// communicate with during the response resolving phase.
+async function resolveMainClient(event) {
+ const client = await self.clients.get(event.clientId)
+
+ if (client?.frameType === 'top-level') {
+ return client
+ }
+
+ const allClients = await self.clients.matchAll({
+ type: 'window',
+ })
+
+ return allClients
+ .filter((client) => {
+ // Get only those clients that are currently visible.
+ return client.visibilityState === 'visible'
+ })
+ .find((client) => {
+ // Find the client ID that's recorded in the
+ // set of clients that have registered the worker.
+ return activeClientIds.has(client.id)
+ })
+}
+
+async function getResponse(event, client, requestId) {
+ const { request } = event
+
+ // Clone the request because it might've been already used
+ // (i.e. its body has been read and sent to the client).
+ const requestClone = request.clone()
+
+ function passthrough() {
+ const headers = Object.fromEntries(requestClone.headers.entries())
+
+ // Remove internal MSW request header so the passthrough request
+ // complies with any potential CORS preflight checks on the server.
+ // Some servers forbid unknown request headers.
+ delete headers['x-msw-intention']
+
+ return fetch(requestClone, { headers })
+ }
+
+ // Bypass mocking when the client is not active.
+ if (!client) {
+ return passthrough()
+ }
+
+ // Bypass initial page load requests (i.e. static assets).
+ // The absence of the immediate/parent client in the map of the active clients
+ // means that MSW hasn't dispatched the "MOCK_ACTIVATE" event yet
+ // and is not ready to handle requests.
+ if (!activeClientIds.has(client.id)) {
+ return passthrough()
+ }
+
+ // Notify the client that a request has been intercepted.
+ const requestBuffer = await request.arrayBuffer()
+ const clientMessage = await sendToClient(
+ client,
+ {
+ type: 'REQUEST',
+ payload: {
+ id: requestId,
+ url: request.url,
+ mode: request.mode,
+ method: request.method,
+ headers: Object.fromEntries(request.headers.entries()),
+ cache: request.cache,
+ credentials: request.credentials,
+ destination: request.destination,
+ integrity: request.integrity,
+ redirect: request.redirect,
+ referrer: request.referrer,
+ referrerPolicy: request.referrerPolicy,
+ body: requestBuffer,
+ keepalive: request.keepalive,
+ },
+ },
+ [requestBuffer],
+ )
+
+ switch (clientMessage.type) {
+ case 'MOCK_RESPONSE': {
+ return respondWithMock(clientMessage.data)
+ }
+
+ case 'PASSTHROUGH': {
+ return passthrough()
+ }
+ }
+
+ return passthrough()
+}
+
+function sendToClient(client, message, transferrables = []) {
+ return new Promise((resolve, reject) => {
+ const channel = new MessageChannel()
+
+ channel.port1.onmessage = (event) => {
+ if (event.data && event.data.error) {
+ return reject(event.data.error)
+ }
+
+ resolve(event.data)
+ }
+
+ client.postMessage(
+ message,
+ [channel.port2].concat(transferrables.filter(Boolean)),
+ )
+ })
+}
+
+async function respondWithMock(response) {
+ // Setting response status code to 0 is a no-op.
+ // However, when responding with a "Response.error()", the produced Response
+ // instance will have status code set to 0. Since it's not possible to create
+ // a Response instance with status code 0, handle that use-case separately.
+ if (response.status === 0) {
+ return Response.error()
+ }
+
+ const mockedResponse = new Response(response.body, response)
+
+ Reflect.defineProperty(mockedResponse, IS_MOCKED_RESPONSE, {
+ value: true,
+ enumerable: true,
+ })
+
+ return mockedResponse
+}
diff --git a/public/vite.svg b/public/vite.svg
new file mode 100644
index 000000000..e7b8dfb1b
--- /dev/null
+++ b/public/vite.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/app.css.ts b/src/app.css.ts
new file mode 100644
index 000000000..28a8be6d8
--- /dev/null
+++ b/src/app.css.ts
@@ -0,0 +1,5 @@
+import { globalStyle } from "@vanilla-extract/css";
+
+globalStyle("html, body", {
+ padding: "0 20% 0 20%",
+});
diff --git a/src/assets/react.svg b/src/assets/react.svg
new file mode 100644
index 000000000..6c87de9bb
--- /dev/null
+++ b/src/assets/react.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/domains/carts/CartsPage/CartsPage.css.ts b/src/domains/carts/CartsPage/CartsPage.css.ts
new file mode 100644
index 000000000..d7403d097
--- /dev/null
+++ b/src/domains/carts/CartsPage/CartsPage.css.ts
@@ -0,0 +1,62 @@
+import { style } from "@vanilla-extract/css";
+
+export const container = style({
+ width: "100%",
+ display: "flex",
+ justifyContent: "center",
+});
+
+export const section = style({
+ display: "flex",
+ alignItems: "center",
+ flexDirection: "column",
+ width: "1320px",
+});
+
+export const titleContainer = style({
+ marginTop: "62px",
+});
+
+export const underLine = style({
+ width: "100%",
+ height: "4px",
+ backgroundColor: "black",
+ margin: "16px 0 55px 0",
+});
+
+export const cartContainer = style({
+ width: "100%",
+ display: "flex",
+ gap: "50px",
+ justifyContent: "space-between",
+});
+
+export const listContainer = style({
+ width: "100%",
+});
+
+export const listNavBox = style({
+ display: "flex",
+ justifyContent: "space-between",
+ alignItems: "center",
+});
+
+export const buttonBox = style({
+ width: "117px",
+ height: "50px",
+});
+
+export const listTitle = style({
+ marginTop: "50px",
+});
+
+export const list = style({
+ display: "flex",
+ flexDirection: "column",
+ gap: "40px",
+});
+
+export const decisionBox = style({
+ width: "448px",
+ height: "318px",
+});
diff --git a/src/domains/carts/CartsPage/CartsPage.tsx b/src/domains/carts/CartsPage/CartsPage.tsx
new file mode 100644
index 000000000..a0393e7db
--- /dev/null
+++ b/src/domains/carts/CartsPage/CartsPage.tsx
@@ -0,0 +1,83 @@
+import Button from "../../shared/components/primitive/Button/Button";
+import DecisionPriceBox from "../../shared/components/domained/DecisionPriceBox/DecisionPriceBox";
+import ProductRowCard from "../../shared/components/domained/ProductRowCard/ProductRowCard";
+
+import useCarts from "../hooks/useCarts";
+import {
+ buttonBox,
+ cartContainer,
+ container,
+ decisionBox,
+ list,
+ listContainer,
+ listNavBox,
+ listTitle,
+ section,
+ titleContainer,
+ underLine,
+} from "./CartsPage.css";
+import Text from "../../shared/components/primitive/Text/Text";
+
+export default function CartsPage() {
+ const {
+ carts,
+ unCheckAll,
+ createCheckProductFunction,
+ changeQuantity,
+ totalPrice,
+ checkedAtLeast,
+ } = useCarts();
+
+ return (
+
+
+
+ 장바구니
+
+
+
+
+
+
+ {" "}
+ 선택해제
+
+
+
+
+
+
든든배송 상품 (3개)
+
+
+ {carts.map(({ product, checked }, index) => {
+ return (
+
+ );
+ })}
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/domains/carts/hooks/useCarts.ts b/src/domains/carts/hooks/useCarts.ts
new file mode 100644
index 000000000..84e172af1
--- /dev/null
+++ b/src/domains/carts/hooks/useCarts.ts
@@ -0,0 +1,74 @@
+import { ChangeEvent, useEffect, useMemo, useState } from "react";
+import { useCarts as useCartsData } from "../../shared/queryHook/carts/carts.query";
+import { Carts } from "../types";
+
+function changeValue(key: keyof T, value: unknown) {
+ return function (obj: T) {
+ obj[key] = value as T[keyof T];
+ return obj;
+ };
+}
+
+function isUnderArrayLength(arr: T[], index: number) {
+ return index < arr.length;
+}
+
+export default function useCarts() {
+ const { data } = useCartsData();
+ const [carts, setCarts] = useState([]);
+
+ function unCheckAll(event: ChangeEvent) {
+ if (!event.target.checked) {
+ const unCheckedThings = carts.map(changeValue("checked", false));
+ setCarts(unCheckedThings);
+ }
+ }
+
+ function createCheckProductFunction(index: number) {
+ if (data && !isUnderArrayLength(carts, index))
+ throw new Error("없는 상품입니다.");
+ return function () {
+ const copied = [...carts];
+ copied[index].checked = !copied[index].checked;
+ setCarts(copied);
+ };
+ }
+
+ function changeQuantity(index: number) {
+ if (data && !isUnderArrayLength(carts, index))
+ throw new Error("없는 상품입니다.");
+ return function (quantity: number) {
+ const copied = [...carts];
+ copied[index].quantity = quantity;
+ setCarts(copied);
+ };
+ }
+
+ const totalPrice = useMemo(() => {
+ return carts.reduce((total, product) => {
+ if (!product.checked) return total;
+ return total + product.quantity * product.product.price;
+ }, 0);
+ }, [carts]);
+
+ const checkedAtLeast = carts.some((cart) => cart.checked);
+
+ useEffect(() => {
+ if (!data?.response) return;
+ const propertiesAddedProducts = data.response.map((product) => ({
+ ...product,
+ quantity: 0,
+ checked: false,
+ }));
+ setCarts(propertiesAddedProducts);
+ }, [data]);
+
+ return {
+ carts,
+ unCheckAll,
+ createCheckProductFunction,
+ changeQuantity,
+ totalPrice,
+ checkedAtLeast,
+ } as const;
+}
diff --git a/src/domains/carts/types.ts b/src/domains/carts/types.ts
new file mode 100644
index 000000000..95016c6d1
--- /dev/null
+++ b/src/domains/carts/types.ts
@@ -0,0 +1,13 @@
+export type Cart = {
+ id: number;
+ quantity: number;
+ checked: boolean;
+ product: {
+ id: number;
+ price: number;
+ name: string;
+ imageUrl: string;
+ };
+};
+
+export type Carts = Cart[];
diff --git a/src/domains/myorder/MyOrderPage/MyOrderPage.css.ts b/src/domains/myorder/MyOrderPage/MyOrderPage.css.ts
new file mode 100644
index 000000000..cad586547
--- /dev/null
+++ b/src/domains/myorder/MyOrderPage/MyOrderPage.css.ts
@@ -0,0 +1,42 @@
+import { style } from "@vanilla-extract/css";
+
+export const container = style({
+ width: "100%",
+ display: "flex",
+ justifyContent: "center",
+});
+
+export const section = style({
+ display: "flex",
+ alignItems: "center",
+ flexDirection: "column",
+ width: "1320px",
+});
+
+export const titleContainer = style({
+ marginTop: "62px",
+ marginBottom: "55px",
+});
+
+export const underLine = style({
+ width: "100%",
+ height: "4px",
+ backgroundColor: "black",
+ margin: "0 0 55px 0",
+});
+
+export const priceSection = style({
+ display: "flex",
+ justifyContent: "flex-end",
+ width: "100%",
+});
+
+export const priceBox = style({
+ width: "500px",
+ marginTop: "55px",
+});
+
+export const priceDetail = style({
+ display: "flex",
+ justifyContent: "space-between",
+});
diff --git a/src/domains/myorder/MyOrderPage/MyOrderPage.tsx b/src/domains/myorder/MyOrderPage/MyOrderPage.tsx
new file mode 100644
index 000000000..df9da6499
--- /dev/null
+++ b/src/domains/myorder/MyOrderPage/MyOrderPage.tsx
@@ -0,0 +1,50 @@
+import { useParams } from "@tanstack/react-router";
+import OrderBox from "../../myorders/components/OrderBox/OrderBox";
+
+import { useOrder } from "../../shared/queryHook/orders/orders.query";
+import {
+ container,
+ priceBox,
+ priceDetail,
+ priceSection,
+ section,
+ titleContainer,
+ underLine,
+} from "./MyOrderPage.css";
+import Text from "../../shared/components/primitive/Text/Text";
+
+export default function MyOrderPage() {
+ const { orderId } = useParams({ strict: false }) as { orderId: string };
+ const { data } = useOrder(Number(orderId));
+
+ if (!data?.response) return null;
+
+ const { id, orderDetails } = data.response;
+
+ const price = orderDetails.reduce((result, order) => {
+ const total = result + order.price * order.quantity;
+ return total;
+ }, 0);
+
+ return (
+
+
+
+ 주문 목록
+
+
+
+
+
+
결제금액 정보
+
+
+
총 결제금액
+
{price.toLocaleString()} 원
+
+
+
+
+
+ );
+}
diff --git a/src/domains/myorders/MyOrdersPage.tsx b/src/domains/myorders/MyOrdersPage.tsx
new file mode 100644
index 000000000..7de6b25d5
--- /dev/null
+++ b/src/domains/myorders/MyOrdersPage.tsx
@@ -0,0 +1,37 @@
+import Text from "../shared/components/primitive/Text/Text";
+import { useOrders } from "../shared/queryHook/orders/orders.query";
+import { Order } from "../shared/queryHook/orders/orders.type";
+import OrderBox from "./components/OrderBox/OrderBox";
+import {
+ container,
+ orderBox,
+ orderSection,
+ section,
+ titleContainer,
+ underLine,
+} from "./MyordersPage.css";
+
+export default function MyOrdersPage() {
+ const { data } = useOrders();
+ return (
+
+
+
+ 주문 목록
+
+
+
+ {data?.response.map(
+ (order: { id: number; orderDetails: Order[] }) => {
+ return (
+
+
+
+ );
+ }
+ )}
+
+
+
+ );
+}
diff --git a/src/domains/myorders/MyordersPage.css.ts b/src/domains/myorders/MyordersPage.css.ts
new file mode 100644
index 000000000..b43a37967
--- /dev/null
+++ b/src/domains/myorders/MyordersPage.css.ts
@@ -0,0 +1,33 @@
+import { style } from "@vanilla-extract/css";
+
+export const container = style({
+ width: "100%",
+ display: "flex",
+ justifyContent: "center",
+});
+
+export const section = style({
+ display: "flex",
+ alignItems: "center",
+ flexDirection: "column",
+ width: "1320px",
+});
+
+export const titleContainer = style({
+ marginTop: "62px",
+});
+
+export const underLine = style({
+ width: "100%",
+ height: "4px",
+ backgroundColor: "black",
+ margin: "55px 0 55px 0",
+});
+
+export const orderSection = style({
+ width: "100%",
+});
+
+export const orderBox = style({
+ marginBottom: "40px",
+});
diff --git a/src/domains/myorders/components/OrderBox/OrderBox.css.ts b/src/domains/myorders/components/OrderBox/OrderBox.css.ts
new file mode 100644
index 000000000..a4d7c68e3
--- /dev/null
+++ b/src/domains/myorders/components/OrderBox/OrderBox.css.ts
@@ -0,0 +1,35 @@
+import { style } from "@vanilla-extract/css";
+
+export const orderContainer = style({
+ width: "100%",
+});
+
+export const orderProductBox = style({
+ padding: "38px 26px",
+ border: "1px solid grey",
+ display: "flex",
+ justifyContent: "space-between",
+ alignItems: "center",
+});
+
+export const orderHeader = style({
+ border: "1px solid grey",
+ backgroundColor: "#f6f6f6",
+ padding: "36px 39px",
+ display: "flex",
+ justifyContent: "space-between",
+ alignItems: "center",
+});
+
+export const alinkReset = style({
+ textDecoration: "none",
+ color: "black",
+ ":active": {
+ textDecoration: "none",
+ },
+});
+
+export const buttonBox = style({
+ width: "138px",
+ height: "47px",
+});
diff --git a/src/domains/myorders/components/OrderBox/OrderBox.tsx b/src/domains/myorders/components/OrderBox/OrderBox.tsx
new file mode 100644
index 000000000..c6f9b5f5a
--- /dev/null
+++ b/src/domains/myorders/components/OrderBox/OrderBox.tsx
@@ -0,0 +1,62 @@
+import { Link } from "@tanstack/react-router";
+import OrderProductRowCard from "../../../shared/components/domained/OrderProductRowCard/OrderProductRowCard";
+import { Order } from "../../../shared/queryHook/orders/orders.type";
+import {
+ alinkReset,
+ buttonBox,
+ orderContainer,
+ orderHeader,
+ orderProductBox,
+} from "./OrderBox.css";
+import Button from "../../../shared/components/primitive/Button/Button";
+import { useAddProductToCart } from "../../../shared/queryHook/carts/carts.query";
+import Text from "../../../shared/components/primitive/Text/Text";
+
+interface OrderBox {
+ id: number;
+ orders: Order[];
+}
+
+export default function OrderBox({ id, orders }: OrderBox) {
+ const { mutate } = useAddProductToCart();
+
+ return (
+
+
+ {`주문번호: ${id}`}
+
+ {"상세보기>"}
+
+
+
+ {orders.map((order) => (
+
+
+
+
+
+
+ ))}
+
+ );
+}
diff --git a/src/domains/myorders/type.ts b/src/domains/myorders/type.ts
new file mode 100644
index 000000000..e69de29bb
diff --git a/src/domains/shared/URLs.ts b/src/domains/shared/URLs.ts
new file mode 100644
index 000000000..c6ca57d1e
--- /dev/null
+++ b/src/domains/shared/URLs.ts
@@ -0,0 +1,7 @@
+export const BASE_URL = import.meta.env.VITE_BASE_URL;
+
+export const URI = {
+ CART_URI: `${BASE_URL}carts`,
+ ORDERS_URI: `${BASE_URL}orders`,
+ PRODUCTS_URI: `${BASE_URL}products`,
+};
diff --git a/src/domains/shared/components/domained/DecisionPriceBox/DecisionPriceBox.css.ts b/src/domains/shared/components/domained/DecisionPriceBox/DecisionPriceBox.css.ts
new file mode 100644
index 000000000..104420b58
--- /dev/null
+++ b/src/domains/shared/components/domained/DecisionPriceBox/DecisionPriceBox.css.ts
@@ -0,0 +1,31 @@
+import { style } from "@vanilla-extract/css";
+
+export const container = style({
+ border: "1px solid rgba(221, 221, 221, 1)",
+ maxWidth: "448px",
+ maxHeight: "318px",
+});
+
+export const titleContainer = style({
+ padding: "22px 30px",
+});
+
+export const underline = style({
+ backgroundColor: "rgba(221, 221, 221, 1)",
+ width: "100%",
+ height: "3px",
+});
+
+export const content = style({
+ padding: "34px 28px",
+});
+
+export const contentPrice = style({
+ display: "flex",
+ justifyContent: "space-between",
+ marginBottom: "68px",
+});
+
+export const buttonBox = style({
+ height: "73px",
+});
diff --git a/src/domains/shared/components/domained/DecisionPriceBox/DecisionPriceBox.stories.ts b/src/domains/shared/components/domained/DecisionPriceBox/DecisionPriceBox.stories.ts
new file mode 100644
index 000000000..1b0b23ad1
--- /dev/null
+++ b/src/domains/shared/components/domained/DecisionPriceBox/DecisionPriceBox.stories.ts
@@ -0,0 +1,19 @@
+import { Meta, StoryObj } from "@storybook/react";
+import DecisionPriceBox from "./DecisionPriceBox";
+const meta: Meta = {
+ component: DecisionPriceBox,
+ title: "DecisionPriceBox",
+};
+
+export default meta;
+
+type Story = StoryObj;
+
+export const Primary: Story = {
+ args: {
+ title: "결제예상금액",
+ subtitle: "결제예상금액",
+ price: 21500,
+ buttonText: "주문하기 (2개)",
+ },
+};
diff --git a/src/domains/shared/components/domained/DecisionPriceBox/DecisionPriceBox.tsx b/src/domains/shared/components/domained/DecisionPriceBox/DecisionPriceBox.tsx
new file mode 100644
index 000000000..a980222e8
--- /dev/null
+++ b/src/domains/shared/components/domained/DecisionPriceBox/DecisionPriceBox.tsx
@@ -0,0 +1,51 @@
+import Button from "../../primitive/Button/Button";
+import Text from "../../primitive/Text/Text";
+
+import {
+ buttonBox,
+ container,
+ content,
+ contentPrice,
+ titleContainer,
+ underline,
+} from "./DecisionPriceBox.css";
+
+interface DecisionPriceBox {
+ title: string;
+ subtitle: string;
+ price: number;
+ buttonText: string;
+ onClick?: () => void;
+}
+
+export default function DecisionPriceBox({
+ title,
+ subtitle,
+ price,
+ buttonText,
+ onClick,
+}: DecisionPriceBox) {
+ return (
+
+
+ {title}
+
+
+
+
+
+
+ {subtitle}
+
+
+
+ {`${price.toLocaleString()} 원`}
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/domains/shared/components/domained/Header/Header.css.ts b/src/domains/shared/components/domained/Header/Header.css.ts
new file mode 100644
index 000000000..96af61708
--- /dev/null
+++ b/src/domains/shared/components/domained/Header/Header.css.ts
@@ -0,0 +1,27 @@
+import { style } from "@vanilla-extract/css";
+
+export const headerBox = style({
+ backgroundColor: "#29c2bc",
+ display: "flex",
+ justifyContent: "center",
+ width: "100vw",
+ boxShadow:
+ "rgba(50, 50, 93, 0.25) 0px 2px 5px -1px, rgba(0, 0, 0, 0.3) 0px 1px 3px -1px",
+});
+
+export const innerContainer = style({
+ width: "70%",
+ display: "flex",
+ justifyContent: "space-between",
+ alignItems: "center",
+});
+
+export const link = style({
+ textDecoration: "none",
+});
+
+export const navList = style({
+ listStyle: "none",
+ display: "flex",
+ gap: "40px",
+});
diff --git a/src/domains/shared/components/domained/Header/Header.stories.tsx b/src/domains/shared/components/domained/Header/Header.stories.tsx
new file mode 100644
index 000000000..f9c2c0daf
--- /dev/null
+++ b/src/domains/shared/components/domained/Header/Header.stories.tsx
@@ -0,0 +1,23 @@
+import { Meta, StoryObj } from "@storybook/react";
+import Header from "./Header";
+
+import { RouterProvider } from "@tanstack/react-router";
+import { router } from "../../../../../routeConfig";
+
+const meta: Meta = {
+ component: Header,
+ title: "Header",
+ decorators: (Story) => {
+ return (
+ } />
+ );
+ },
+};
+
+export default meta;
+
+type Story = StoryObj;
+
+export const Primary: Story = {
+ args: {},
+};
diff --git a/src/domains/shared/components/domained/Header/Header.tsx b/src/domains/shared/components/domained/Header/Header.tsx
new file mode 100644
index 000000000..774a81403
--- /dev/null
+++ b/src/domains/shared/components/domained/Header/Header.tsx
@@ -0,0 +1,37 @@
+import { Link } from "@tanstack/react-router";
+import { headerBox, innerContainer, link, navList } from "./Header.css";
+import Text from "../../primitive/Text/Text";
+
+export default function Header() {
+ return (
+
+
+
+
+
+ NEXTSTEP
+
+
+
+
+
+ -
+
+
+ 장바구니
+
+
+
+ -
+
+
+ 주문목록
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/domains/shared/components/domained/OrderProductRowCard/OrderProductRowCard.css.ts b/src/domains/shared/components/domained/OrderProductRowCard/OrderProductRowCard.css.ts
new file mode 100644
index 000000000..5746148ac
--- /dev/null
+++ b/src/domains/shared/components/domained/OrderProductRowCard/OrderProductRowCard.css.ts
@@ -0,0 +1,21 @@
+import { style } from "@vanilla-extract/css";
+
+export const cardContainer = style({
+ maxWidth: "740px",
+ maxHeight: "178px",
+ display: "flex",
+});
+
+export const cardImg = style({
+ maxHeight: "120px",
+ maxWidth: "120px",
+ objectFit: "cover",
+});
+
+export const cardInfo = style({
+ marginLeft: "12px",
+});
+
+export const cardQuantity = style({
+ marginTop: "4px",
+});
diff --git a/src/domains/shared/components/domained/OrderProductRowCard/OrderProductRowCard.stories.ts b/src/domains/shared/components/domained/OrderProductRowCard/OrderProductRowCard.stories.ts
new file mode 100644
index 000000000..bd33005c9
--- /dev/null
+++ b/src/domains/shared/components/domained/OrderProductRowCard/OrderProductRowCard.stories.ts
@@ -0,0 +1,21 @@
+import { Meta, StoryObj } from "@storybook/react";
+import OrderProductRowCard from "./OrderProductRowCard";
+
+const meta: Meta = {
+ component: OrderProductRowCard,
+ title: "OrderProductRowCard",
+};
+
+export default meta;
+
+type Story = StoryObj;
+
+export const Primary: Story = {
+ args: {
+ imgSrc:
+ "https://s3-alpha-sig.figma.com/img/05ef/e578/d81445480aff1872344a6b1b35323488?Expires=1712534400&Key-Pair-Id=APKAQ4GOSFWCVNEHN3O4&Signature=lDIOdTULo6prhJrybK5LdMhKfA3YgOvxcSLO1WNBpNUpu0hqm-Y1ROsZsVB3S5o~P5USGru86dkRkoL9ATGDL0cyOaeSSolXGfjjMllszONpDqTfwfjB8kgKRqhko25QkqLCwvbX0mSusk8yh78QMHpwTIRA5KMYtQ10F68J088LK0WUDabWGHe~HeZFyrdwess1eQoCg60VvFW2Rhph8qdgqiAu6JHzOmA4Wi1rcrDVbXHk5kEwvDCdYXh-EG6-rZgaH6r3ian9Wlr-12SZY3TCzIlbbAUDvhS985~yHWwXNRQGeFdSmTwdKHA3qadmEMD7jx4eRwG6EgF0bAnHUg__",
+ title: "PET보틀-정사각(420ml)",
+ quantity: 3,
+ price: 21000,
+ },
+};
diff --git a/src/domains/shared/components/domained/OrderProductRowCard/OrderProductRowCard.tsx b/src/domains/shared/components/domained/OrderProductRowCard/OrderProductRowCard.tsx
new file mode 100644
index 000000000..940956f7a
--- /dev/null
+++ b/src/domains/shared/components/domained/OrderProductRowCard/OrderProductRowCard.tsx
@@ -0,0 +1,37 @@
+import Text from "../../primitive/Text/Text";
+import {
+ cardContainer,
+ cardImg,
+ cardInfo,
+ cardQuantity,
+} from "./OrderProductRowCard.css";
+
+interface OrderProductRowCard {
+ imgSrc: string;
+ title: string;
+ quantity: number;
+ price: number;
+}
+
+export default function OrderProductRowCard({
+ imgSrc,
+ title,
+ quantity,
+ price,
+}: OrderProductRowCard) {
+ return (
+
+
+

+
+
+
+ {title}
+
+
+ {`${price.toLocaleString()}원 / 수량: ${quantity.toLocaleString()}`}
+
+
+
+ );
+}
diff --git a/src/domains/shared/components/domained/ProductCard/ProductCard.css.ts b/src/domains/shared/components/domained/ProductCard/ProductCard.css.ts
new file mode 100644
index 000000000..822f5c5d9
--- /dev/null
+++ b/src/domains/shared/components/domained/ProductCard/ProductCard.css.ts
@@ -0,0 +1,19 @@
+import { style } from "@vanilla-extract/css";
+
+export const CardContainer = style({
+ maxWidth: "280px",
+ maxHeight: "360px",
+});
+
+export const CardImg = style({
+ width: "100%",
+ height: "100%",
+ objectFit: "cover",
+});
+
+export const CardInfo = style({
+ display: "flex",
+ justifyContent: "space-between",
+ alignItems: "center",
+ padding: "18px 12px",
+});
diff --git a/src/domains/shared/components/domained/ProductCard/ProductCard.stories.ts b/src/domains/shared/components/domained/ProductCard/ProductCard.stories.ts
new file mode 100644
index 000000000..cbafd2360
--- /dev/null
+++ b/src/domains/shared/components/domained/ProductCard/ProductCard.stories.ts
@@ -0,0 +1,20 @@
+import { Meta, StoryObj } from "@storybook/react";
+import ProductCard from "./ProductCard";
+
+const meta: Meta = {
+ component: ProductCard,
+ title: "ProductCard",
+};
+
+export default meta;
+
+type Story = StoryObj;
+
+export const Primary: Story = {
+ args: {
+ title: "PET보틀-정사각(420ml)",
+ price: 43400,
+ imageUrl:
+ "https://s3-alpha-sig.figma.com/img/05ef/e578/d81445480aff1872344a6b1b35323488?Expires=1712534400&Key-Pair-Id=APKAQ4GOSFWCVNEHN3O4&Signature=lDIOdTULo6prhJrybK5LdMhKfA3YgOvxcSLO1WNBpNUpu0hqm-Y1ROsZsVB3S5o~P5USGru86dkRkoL9ATGDL0cyOaeSSolXGfjjMllszONpDqTfwfjB8kgKRqhko25QkqLCwvbX0mSusk8yh78QMHpwTIRA5KMYtQ10F68J088LK0WUDabWGHe~HeZFyrdwess1eQoCg60VvFW2Rhph8qdgqiAu6JHzOmA4Wi1rcrDVbXHk5kEwvDCdYXh-EG6-rZgaH6r3ian9Wlr-12SZY3TCzIlbbAUDvhS985~yHWwXNRQGeFdSmTwdKHA3qadmEMD7jx4eRwG6EgF0bAnHUg__",
+ },
+};
diff --git a/src/domains/shared/components/domained/ProductCard/ProductCard.tsx b/src/domains/shared/components/domained/ProductCard/ProductCard.tsx
new file mode 100644
index 000000000..62f7a36ab
--- /dev/null
+++ b/src/domains/shared/components/domained/ProductCard/ProductCard.tsx
@@ -0,0 +1,45 @@
+import { MouseEvent } from "react";
+import IconButton from "../../primitive/IconButton/IconButton";
+import { CardContainer, CardImg, CardInfo } from "./ProductCard.css";
+
+interface ProductCard {
+ imageUrl: string;
+ title: string;
+ price: number;
+ onAddClick?: () => void;
+}
+
+export default function ProductCard({
+ imageUrl,
+ title,
+ price,
+ onAddClick,
+}: ProductCard) {
+ function add(e: MouseEvent) {
+ e.preventDefault();
+ e.stopPropagation();
+ onAddClick?.();
+ }
+ return (
+
+
+

+
+
+
+
{title}
+
{price.toLocaleString()} 원
+
+
+
+
+
+
+ );
+}
diff --git a/src/domains/shared/components/domained/ProductRowCard/ProductRowCard.css.ts b/src/domains/shared/components/domained/ProductRowCard/ProductRowCard.css.ts
new file mode 100644
index 000000000..60d13d2c8
--- /dev/null
+++ b/src/domains/shared/components/domained/ProductRowCard/ProductRowCard.css.ts
@@ -0,0 +1,33 @@
+import { style } from "@vanilla-extract/css";
+
+export const cardContainer = style({
+ maxWidth: "740px",
+ maxHeight: "178px",
+ display: "flex",
+ justifyContent: "space-between",
+});
+
+export const cardLeftSection = style({
+ display: "flex",
+});
+
+export const cardDescription = style({
+ display: "flex",
+});
+
+export const cardImg = style({
+ maxWidth: "150%",
+ height: "100%",
+ objectFit: "cover",
+});
+
+export const cardTitle = style({
+ marginLeft: "10px",
+});
+
+export const cardFeatureContainer = style({
+ textAlign: "end",
+ display: "flex",
+ justifyContent: "space-between",
+ flexDirection: "column",
+});
diff --git a/src/domains/shared/components/domained/ProductRowCard/ProductRowCard.stories.ts b/src/domains/shared/components/domained/ProductRowCard/ProductRowCard.stories.ts
new file mode 100644
index 000000000..a7bd36a21
--- /dev/null
+++ b/src/domains/shared/components/domained/ProductRowCard/ProductRowCard.stories.ts
@@ -0,0 +1,21 @@
+import { Meta, StoryObj } from "@storybook/react";
+import ProductRowCard from "./ProductRowCard";
+
+const meta: Meta = {
+ component: ProductRowCard,
+ title: "ProductRowCard",
+};
+
+export default meta;
+
+type Story = StoryObj;
+
+export const Primary: Story = {
+ args: {
+ productImgUrl:
+ "https://s3-alpha-sig.figma.com/img/05ef/e578/d81445480aff1872344a6b1b35323488?Expires=1712534400&Key-Pair-Id=APKAQ4GOSFWCVNEHN3O4&Signature=lDIOdTULo6prhJrybK5LdMhKfA3YgOvxcSLO1WNBpNUpu0hqm-Y1ROsZsVB3S5o~P5USGru86dkRkoL9ATGDL0cyOaeSSolXGfjjMllszONpDqTfwfjB8kgKRqhko25QkqLCwvbX0mSusk8yh78QMHpwTIRA5KMYtQ10F68J088LK0WUDabWGHe~HeZFyrdwess1eQoCg60VvFW2Rhph8qdgqiAu6JHzOmA4Wi1rcrDVbXHk5kEwvDCdYXh-EG6-rZgaH6r3ian9Wlr-12SZY3TCzIlbbAUDvhS985~yHWwXNRQGeFdSmTwdKHA3qadmEMD7jx4eRwG6EgF0bAnHUg__",
+ title: "PET보틀-정사각(420ml)",
+ price: 43400,
+ checked: false,
+ },
+};
diff --git a/src/domains/shared/components/domained/ProductRowCard/ProductRowCard.tsx b/src/domains/shared/components/domained/ProductRowCard/ProductRowCard.tsx
new file mode 100644
index 000000000..f52f9cfd1
--- /dev/null
+++ b/src/domains/shared/components/domained/ProductRowCard/ProductRowCard.tsx
@@ -0,0 +1,70 @@
+import { ChangeEvent } from "react";
+import Count from "../../primitive/Count/Count";
+import IconButton from "../../primitive/IconButton/IconButton";
+import {
+ cardContainer,
+ cardDescription,
+ cardFeatureContainer,
+ cardImg,
+ cardLeftSection,
+ cardTitle,
+} from "./ProductRowCard.css";
+
+interface ProductRowCard {
+ productImgUrl: string;
+ title: string;
+ price: number;
+ quanity?: number;
+ onChangeQuanity?: (quanity: number) => void;
+ onRemove?: () => void;
+ onCheck?: (value: boolean) => void;
+ checked?: boolean;
+}
+
+export default function ProductRowCard({
+ productImgUrl,
+ title,
+ price,
+ quanity,
+ onChangeQuanity,
+ onRemove,
+ onCheck,
+ checked,
+}: ProductRowCard) {
+ function check(event: ChangeEvent) {
+ onCheck?.(event.target.checked);
+ }
+
+ return (
+
+
+
+
+
+

+
+
{title}
+
+
+
+
+
+
+
+
+
+
{price.toLocaleString()} 원
+
+
+ );
+}
diff --git a/src/domains/shared/components/primitive/Button/Button.css.ts b/src/domains/shared/components/primitive/Button/Button.css.ts
new file mode 100644
index 000000000..b4dd61fe2
--- /dev/null
+++ b/src/domains/shared/components/primitive/Button/Button.css.ts
@@ -0,0 +1,95 @@
+import { recipe } from "@vanilla-extract/recipes";
+
+export const buttonStyle = recipe({
+ variants: {
+ appearance: {
+ primary: {},
+ secondary: {},
+ outline: {},
+ },
+ size: {
+ xlg: {
+ fontSize: "32px",
+ fontWeight: "700",
+ },
+ lg: {
+ fontSize: "24px",
+ fontWeight: "400",
+ },
+ md: {
+ fontSize: "20px",
+ fontWeight: "4 00",
+ },
+ sm: {},
+ },
+ onlyText: {
+ true: {},
+ false: {},
+ },
+ },
+
+ compoundVariants: [
+ {
+ variants: {
+ appearance: "primary",
+ onlyText: true,
+ },
+ style: { backgroundColor: "transparent", color: "#29c2bc" },
+ },
+ {
+ variants: {
+ appearance: "primary",
+ onlyText: false,
+ },
+ style: { backgroundColor: "#29c2bc" },
+ },
+ {
+ variants: {
+ appearance: "secondary",
+ onlyText: true,
+ },
+ style: { backgroundColor: "transparent", color: "#73675d" },
+ },
+ {
+ variants: {
+ appearance: "secondary",
+ onlyText: false,
+ },
+ style: {
+ backgroundColor: "#73675d",
+ },
+ },
+ {
+ variants: {
+ appearance: "outline",
+ onlyText: true,
+ },
+ style: {
+ backgroundColor: "transparent",
+ color: "black",
+ },
+ },
+ {
+ variants: {
+ appearance: "outline",
+ onlyText: false,
+ },
+ style: {
+ backgroundColor: "transparent",
+ border: "1px solid grey",
+ color: "black",
+ },
+ },
+ ],
+ base: {
+ width: "100%",
+ height: "100%",
+ outline: "none",
+ border: 0,
+ display: "flex",
+ justifyContent: "center",
+ alignItems: "center",
+ color: "white",
+ cursor: "pointer",
+ },
+});
diff --git a/src/domains/shared/components/primitive/Button/Button.stories.ts b/src/domains/shared/components/primitive/Button/Button.stories.ts
new file mode 100644
index 000000000..75d58cdc0
--- /dev/null
+++ b/src/domains/shared/components/primitive/Button/Button.stories.ts
@@ -0,0 +1,35 @@
+import { Meta, StoryObj } from "@storybook/react";
+import Button from "./Button";
+
+const meta: Meta = {
+ component: Button,
+ title: "Button",
+};
+
+export default meta;
+
+type Story = StoryObj;
+
+export const Primary: Story = {
+ args: {
+ size: "xlg",
+ children: "button",
+ onlyText: true,
+ },
+};
+
+export const Secondary: Story = {
+ args: {
+ appearance: "secondary",
+ children: "button",
+ onlyText: true,
+ },
+};
+
+export const Outline: Story = {
+ args: {
+ appearance: "outline",
+ children: "button",
+ onlyText: true,
+ },
+};
diff --git a/src/domains/shared/components/primitive/Button/Button.tsx b/src/domains/shared/components/primitive/Button/Button.tsx
new file mode 100644
index 000000000..f4595218c
--- /dev/null
+++ b/src/domains/shared/components/primitive/Button/Button.tsx
@@ -0,0 +1,26 @@
+import { ComponentProps } from "react";
+import { buttonStyle } from "./Button.css";
+
+interface Button extends ComponentProps<"button"> {
+ appearance?: "primary" | "secondary" | "outline";
+ size?: "xlg" | "lg" | "md" | "sm";
+ onlyText?: boolean;
+}
+
+export default function Button(props: Button) {
+ const {
+ appearance = "primary",
+ children,
+ onlyText = false,
+ size = "md",
+ ...rest
+ } = props;
+ return (
+
+ );
+}
diff --git a/src/domains/shared/components/primitive/Count/Count.css.ts b/src/domains/shared/components/primitive/Count/Count.css.ts
new file mode 100644
index 000000000..fcfced9af
--- /dev/null
+++ b/src/domains/shared/components/primitive/Count/Count.css.ts
@@ -0,0 +1,21 @@
+import { style } from "@vanilla-extract/css";
+
+export const countContainer = style({
+ display: "flex",
+ width: "114px",
+ height: "60",
+ border: "1px solid grey",
+ justifyContent: "space-between",
+});
+
+export const countText = style({
+ display: "flex",
+ justifyContent: "center",
+ alignItems: "center",
+ width: "100%",
+});
+
+export const button = style({
+ width: "42px",
+ height: "30px",
+});
diff --git a/src/domains/shared/components/primitive/Count/Count.stories.ts b/src/domains/shared/components/primitive/Count/Count.stories.ts
new file mode 100644
index 000000000..fef6dae74
--- /dev/null
+++ b/src/domains/shared/components/primitive/Count/Count.stories.ts
@@ -0,0 +1,18 @@
+import Count from "./Count";
+import { Meta, StoryObj } from "@storybook/react";
+
+const meta: Meta = {
+ component: Count,
+ title: "Count",
+};
+
+export default meta;
+
+type Story = StoryObj;
+
+export const Primary: Story = {
+ args: {
+ min: -10,
+ max: 10,
+ },
+};
diff --git a/src/domains/shared/components/primitive/Count/Count.tsx b/src/domains/shared/components/primitive/Count/Count.tsx
new file mode 100644
index 000000000..cdb57afba
--- /dev/null
+++ b/src/domains/shared/components/primitive/Count/Count.tsx
@@ -0,0 +1,65 @@
+import { useEffect, useState } from "react";
+import Button from "../Button/Button";
+import { button, countContainer, countText } from "./Count.css";
+import Text from "../Text/Text";
+
+interface Count {
+ max?: number;
+ min?: number;
+ onChange?: (value: number) => void;
+ value?: number;
+}
+
+export default function Count({
+ max = Infinity,
+ min = -Infinity,
+ onChange,
+ value,
+}: Count) {
+ const [countValue, setCountValue] = useState(0);
+
+ useEffect(() => {
+ onChange?.(value ?? countValue);
+ }, [countValue, value, onChange]);
+
+ function changeCountValue(value: number) {
+ onChange?.(value ?? countValue);
+ setCountValue(value);
+ }
+
+ function up() {
+ if (countValue >= max) {
+ changeCountValue(max);
+ } else {
+ changeCountValue(countValue + 1);
+ }
+ }
+
+ function down() {
+ if (countValue <= min) {
+ changeCountValue(min);
+ } else {
+ changeCountValue(countValue - 1);
+ }
+ }
+
+ return (
+
+
+ {value ?? countValue}
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/domains/shared/components/primitive/IconButton/IconButton.css.ts b/src/domains/shared/components/primitive/IconButton/IconButton.css.ts
new file mode 100644
index 000000000..6f32d7e45
--- /dev/null
+++ b/src/domains/shared/components/primitive/IconButton/IconButton.css.ts
@@ -0,0 +1,9 @@
+import { recipe } from "@vanilla-extract/recipes";
+
+export const IconButtonStyle = recipe({
+ base: {
+ cursor: "pointer",
+ backgroundColor: "transparent",
+ border: "none",
+ },
+});
diff --git a/src/domains/shared/components/primitive/IconButton/IconButton.stories.ts b/src/domains/shared/components/primitive/IconButton/IconButton.stories.ts
new file mode 100644
index 000000000..11a210206
--- /dev/null
+++ b/src/domains/shared/components/primitive/IconButton/IconButton.stories.ts
@@ -0,0 +1,20 @@
+import { Meta, StoryObj } from "@storybook/react";
+import IconButton from "./IconButton";
+
+const meta: Meta = {
+ component: IconButton,
+ title: "IconButton",
+};
+
+export default meta;
+
+type Story = StoryObj;
+
+export const Normarl: Story = {
+ args: {
+ width: 10,
+ height: 10,
+ src: "/vite.svg",
+ alt: "icon image",
+ },
+};
diff --git a/src/domains/shared/components/primitive/IconButton/IconButton.tsx b/src/domains/shared/components/primitive/IconButton/IconButton.tsx
new file mode 100644
index 000000000..66ba8b303
--- /dev/null
+++ b/src/domains/shared/components/primitive/IconButton/IconButton.tsx
@@ -0,0 +1,24 @@
+import { ButtonHTMLAttributes } from "react";
+import { IconButtonStyle } from "./IconButton.css";
+
+interface IconButton
+ extends Omit, "children"> {
+ width: number;
+ height: number;
+ src: string;
+ alt: string;
+}
+
+export default function IconButton({
+ width,
+ height,
+ src,
+ alt,
+ ...rest
+}: IconButton) {
+ return (
+
+ );
+}
diff --git a/src/domains/shared/components/primitive/Loader/Loader.tsx b/src/domains/shared/components/primitive/Loader/Loader.tsx
new file mode 100644
index 000000000..671276315
--- /dev/null
+++ b/src/domains/shared/components/primitive/Loader/Loader.tsx
@@ -0,0 +1,3 @@
+export default function Loader() {
+ return Loading...
;
+}
diff --git a/src/domains/shared/components/primitive/Text/Text.css.ts b/src/domains/shared/components/primitive/Text/Text.css.ts
new file mode 100644
index 000000000..fb206eb3d
--- /dev/null
+++ b/src/domains/shared/components/primitive/Text/Text.css.ts
@@ -0,0 +1,24 @@
+import { recipe } from "@vanilla-extract/recipes";
+
+export const IconButtonStyle = recipe({
+ variants: {
+ weight: {
+ 900: {
+ fontWeight: "900",
+ },
+ 700: { fontWeight: "700" },
+ 400: {
+ fontWeight: "400",
+ },
+ },
+ as: {
+ title: {
+ fontSize: "40px",
+ },
+ subtitle: { fontSize: "32px" },
+ body: { fontSize: "24px" },
+ description: { fontSize: "20px" },
+ caption: { fontSize: "16px" },
+ },
+ },
+});
diff --git a/src/domains/shared/components/primitive/Text/Text.stories.ts b/src/domains/shared/components/primitive/Text/Text.stories.ts
new file mode 100644
index 000000000..541aac917
--- /dev/null
+++ b/src/domains/shared/components/primitive/Text/Text.stories.ts
@@ -0,0 +1,19 @@
+import { Meta, StoryObj } from "@storybook/react";
+import Text from "./Text";
+
+const meta: Meta = {
+ component: Text,
+ title: "Text",
+};
+
+export default meta;
+
+type Story = StoryObj;
+
+export const Primary: Story = {
+ args: {
+ color: "blue",
+ children: "text",
+ as: "title",
+ },
+};
diff --git a/src/domains/shared/components/primitive/Text/Text.tsx b/src/domains/shared/components/primitive/Text/Text.tsx
new file mode 100644
index 000000000..1b62b377a
--- /dev/null
+++ b/src/domains/shared/components/primitive/Text/Text.tsx
@@ -0,0 +1,24 @@
+import { createElement, ReactHTML } from "react";
+import { IconButtonStyle } from "./Text.css";
+
+interface Text {
+ tag?: keyof ReactHTML;
+ as: "title" | "subtitle" | "body" | "description" | "caption";
+ children: string | number;
+ color?: string;
+ weight?: 900 | 700 | 400;
+}
+
+export default function Text({
+ tag = "div",
+ color = "black",
+ children,
+ as,
+ weight = 400,
+}: Text) {
+ return createElement(tag, {
+ style: { color },
+ className: IconButtonStyle({ as, weight }),
+ children,
+ });
+}
diff --git a/src/domains/shared/constants.ts b/src/domains/shared/constants.ts
new file mode 100644
index 000000000..8b1378917
--- /dev/null
+++ b/src/domains/shared/constants.ts
@@ -0,0 +1 @@
+
diff --git a/src/domains/shared/queryHook/carts/carts.clents.ts b/src/domains/shared/queryHook/carts/carts.clents.ts
new file mode 100644
index 000000000..7838d48fd
--- /dev/null
+++ b/src/domains/shared/queryHook/carts/carts.clents.ts
@@ -0,0 +1,31 @@
+import { z } from "zod";
+import { cartsScheme } from "./carts.type";
+import { Product, Response, responseScheme } from "../types";
+import { URI } from "../../URLs";
+
+class CartsRepository {
+ async getCarts() {
+ const response = await fetch(URI.CART_URI);
+ const responseData = await response.json();
+ return responseScheme(cartsScheme).parse(responseData);
+ }
+
+ async addToCart(product: Product) {
+ const response = await fetch(URI.CART_URI, {
+ method: "POST",
+ body: JSON.stringify({ product }),
+ });
+
+ const responseData: Response