์ฝ˜ํ…์ธ  ๋Œ€ํ‘œ ์ด๋ฏธ์ง€ - ๐Ÿ›๏ธ ๋น„ํšŒ์› ์ฃผ๋ฌธ & ๊ฒŒ์ŠคํŠธ ์ฒดํฌ์•„์›ƒ ์™„์ „ ์ •๋ณตํšŒ์›๊ฐ€์ž… ์—†์ด๋„ ์ฒ™์ฒ™! ์‡ผํ•‘๋ชฐ ์ „ํ™˜์œจ์„ ๋†’์ด๋Š” ๊ตฌํ˜„ ์ „๋žต
๐Ÿ›’ ์‡ผํ•‘๋ชฐ ๊ฐœ๋ฐœ ยท ํ”„๋กœ๊ทธ๋žจ๊ฐœ๋ฐœ

๐Ÿ›๏ธ ๋น„ํšŒ์› ์ฃผ๋ฌธ & ๊ฒŒ์ŠคํŠธ ์ฒดํฌ์•„์›ƒ ์™„์ „ ์ •๋ณต
ํšŒ์›๊ฐ€์ž… ์—†์ด๋„ ์ฒ™์ฒ™! ์‡ผํ•‘๋ชฐ ์ „ํ™˜์œจ์„ ๋†’์ด๋Š” ๊ตฌํ˜„ ์ „๋žต

๋กœ๊ทธ์ธ ์žฅ๋ฒฝ์„ ๋‚ฎ์ถ”๊ณ  ๋งค์ถœ์„ ์˜ฌ๋ฆฌ๋Š” ์‹ค์ „ ๊ฐœ๋ฐœ ๊ฐ€์ด๋“œ ๐Ÿš€

์•ผ, ์†”์งํžˆ ๋งํ•ด๋ด. ๐Ÿค”
์‡ผํ•‘๋ชฐ์—์„œ ๋ญ”๊ฐ€ ์‚ฌ๋ ค๊ณ  ํ–ˆ๋Š”๋ฐ "ํšŒ์›๊ฐ€์ž… ๋จผ์ € ํ•˜์„ธ์š”!" ๋ผ๋Š” ๋ฌธ๊ตฌ ๋ณด๊ณ  ๊ทธ๋ƒฅ ๋’ค๋กœ๊ฐ€๊ธฐ ๋ˆ„๋ฅธ ์  ์žˆ์ง€ ์•Š์•„?
๋‚˜๋งŒ ๊ทธ๋Ÿฐ ๊ฑฐ ์•„๋‹ˆ์•ผ. ํ†ต๊ณ„์ ์œผ๋กœ ์‡ผํ•‘๋ชฐ ์ดํƒˆ๋ฅ ์˜ ์•ฝ 23~35%๊ฐ€ ๋ฐ”๋กœ ์ด '๊ฐ•์ œ ํšŒ์›๊ฐ€์ž…' ๋•Œ๋ฌธ์— ๋ฐœ์ƒํ•œ๋‹ค๊ณ  ํ•ด.
๊ทธ๋ž˜์„œ ์š”์ฆ˜ ์ž˜ ๋‚˜๊ฐ€๋Š” ์‡ผํ•‘๋ชฐ๋“ค์€ ๊ฒŒ์ŠคํŠธ ์ฒดํฌ์•„์›ƒ(Guest Checkout), ์ฆ‰ ๋น„ํšŒ์› ์ฃผ๋ฌธ ๊ธฐ๋Šฅ์„ ํ•„์ˆ˜๋กœ ๋„ฃ๊ณ  ์žˆ์–ด.

์ด ๊ธ€์—์„œ๋Š” ๋น„ํšŒ์› ์ฃผ๋ฌธ์ด ์™œ ์ค‘์š”ํ•œ์ง€๋ถ€ํ„ฐ, ์‹ค์ œ๋กœ ์–ด๋–ป๊ฒŒ ๊ตฌํ˜„ํ•˜๋Š”์ง€๊นŒ์ง€ ๊ฐœ๋ฐœ์ž ๊ด€์ ์—์„œ ํŒฉํŠธ ์œ„์ฃผ๋กœ ์ซ™ ํ’€์–ด๋ณผ๊ฒŒ.
๋ฐฑ์—”๋“œ ๋กœ์ง, ์„ธ์…˜ ๊ด€๋ฆฌ, ๋ณด์•ˆ ์ฒ˜๋ฆฌ, ์ฃผ๋ฌธ ์กฐํšŒ๊นŒ์ง€ โ€” ํ•˜๋‚˜๋„ ๋น ์ง์—†์ด ๋‹ค ๋‹ค๋ฃฐ ๊ฑฐ์•ผ. ์ค€๋น„๋์ง€? ๐Ÿ”ฅ

๐Ÿ›’ ํšŒ์› ์ฃผ๋ฌธ vs ๋น„ํšŒ์›(๊ฒŒ์ŠคํŠธ) ์ฃผ๋ฌธ ๋น„๊ต ๐Ÿ˜ค ๊ฐ•์ œ ํšŒ์›๊ฐ€์ž… ๋ฐฉ์‹ 1 ์ƒํ’ˆ ์„ ํƒ 2 ํšŒ์›๊ฐ€์ž… ์š”๊ตฌ ๐Ÿ˜ฉ 3 ์ด๋ฉ”์ผ ์ธ์ฆ ๋Œ€๊ธฐ โณ 4 ๋กœ๊ทธ์ธ ํ›„ ๋‹ค์‹œ ์žฅ๋ฐ”๊ตฌ๋‹ˆ ์ดํƒˆ๋ฅ  โ†‘โ†‘ ๐Ÿ˜ญ ๐Ÿ˜Š ๊ฒŒ์ŠคํŠธ ์ฒดํฌ์•„์›ƒ ๋ฐฉ์‹ 1 ์ƒํ’ˆ ์„ ํƒ 2 ์ด๋ฉ”์ผ๋งŒ ์ž…๋ ฅ โœ‰๏ธ 3 ๋ฐฐ์†ก์ง€ ์ž…๋ ฅ ๐Ÿ“ฆ 4 ๊ฒฐ์ œ ์™„๋ฃŒ ๐ŸŽ‰ ์ „ํ™˜์œจ โ†‘โ†‘ ๐Ÿš€ VS

๐Ÿ“Š ์™œ ๊ฒŒ์ŠคํŠธ ์ฒดํฌ์•„์›ƒ์ด ํ•„์ˆ˜์ธ๊ฐ€? โ€” ์ˆซ์ž๋กœ ๋ณด๋Š” ํ˜„์‹ค

๊ฐœ๋ฐœ์ž ์ž…์žฅ์—์„œ "๊ทธ๋ƒฅ ํšŒ์›๊ฐ€์ž… ์‹œํ‚ค๋ฉด ๋˜์ง€ ์•Š๋‚˜?" ์‹ถ์„ ์ˆ˜ ์žˆ์–ด.
๊ทผ๋ฐ ๋งˆ์ผ€ํŒ…ยทUX ๋ฐ์ดํ„ฐ๋ฅผ ๋ณด๋ฉด ์ƒ๊ฐ์ด ๋‹ฌ๋ผ์งˆ ๊ฑฐ์•ผ.

๐Ÿ“Œ Baymard Institute ์—ฐ๊ตฌ ๊ฒฐ๊ณผ (2023)
์‡ผํ•‘๋ชฐ ์žฅ๋ฐ”๊ตฌ๋‹ˆ ์ดํƒˆ ์›์ธ 1์œ„: "์‚ฌ์ดํŠธ๊ฐ€ ๊ณ„์ • ์ƒ์„ฑ์„ ๊ฐ•์š”ํ•ด์„œ" (24%)
๊ฒŒ์ŠคํŠธ ์ฒดํฌ์•„์›ƒ ์ œ๊ณต ์‹œ ์ „ํ™˜์œจ ํ‰๊ท  35% ํ–ฅ์ƒ
Amazon, Shopify, WooCommerce ๋“ฑ ์ฃผ์š” ํ”Œ๋žซํผ ๋ชจ๋‘ ๊ธฐ๋ณธ ์ œ๊ณต

ํŠนํžˆ ๋ชจ๋ฐ”์ผ ์‡ผํ•‘ ํ™˜๊ฒฝ์—์„œ๋Š” ๋” ์‹ฌ๊ฐํ•ด.
์ž‘์€ ํ™”๋ฉด์—์„œ ํšŒ์›๊ฐ€์ž… ํผ ๋‹ค ์ฑ„์šฐ๋Š” ๊ฑฐ ์ง„์งœ ๊ท€์ฐฎ๊ฑฐ๋“ .
๊ทธ๋ž˜์„œ ๋ชจ๋ฐ”์ผ ์ „ํ™˜์œจ์„ ๋†’์ด๋ ค๋ฉด ๊ฒŒ์ŠคํŠธ ์ฒดํฌ์•„์›ƒ์€ ์„ ํƒ์ด ์•„๋‹ˆ๋ผ ํ•„์ˆ˜์•ผ.

๐Ÿ˜ค
๊ฐ•์ œ ํšŒ์›๊ฐ€์ž…
์ดํƒˆ๋ฅ  35% ์ฆ๊ฐ€
๐ŸŽฏ
๊ฒŒ์ŠคํŠธ ์ฒดํฌ์•„์›ƒ
์ „ํ™˜์œจ 35% ํ–ฅ์ƒ
๐Ÿ’ฐ
๋งค์ถœ ์ฆ๊ฐ€
ํ‰๊ท  ์ฃผ๋ฌธ ์™„๋ฃŒ์œจ โ†‘
๐Ÿ”„
์žฌ๋ฐฉ๋ฌธ ์œ ๋„
์ฃผ๋ฌธ ํ›„ ๊ฐ€์ž… ์ „ํ™˜

๐Ÿ—๏ธ ๊ฒŒ์ŠคํŠธ ์ฒดํฌ์•„์›ƒ ์„ค๊ณ„์˜ ํ•ต์‹ฌ ๊ฐœ๋…

์ž, ์ด์ œ ์ง„์งœ ๊ฐœ๋ฐœ ์–˜๊ธฐ ๋“ค์–ด๊ฐ€๋ณด์ž.
๊ฒŒ์ŠคํŠธ ์ฒดํฌ์•„์›ƒ์„ ๊ตฌํ˜„ํ•  ๋•Œ ๊ฐ€์žฅ ๋จผ์ € ์ดํ•ดํ•ด์•ผ ํ•  ๊ฑด "๋น„ํšŒ์›์„ ์–ด๋–ป๊ฒŒ ์‹๋ณ„ํ•˜๊ณ  ์ถ”์ ํ•˜๋А๋ƒ"์•ผ.

๐Ÿ”‘ ๋น„ํšŒ์› ์‹๋ณ„ ์ „๋žต 3๊ฐ€์ง€

โ‘  ์„ธ์…˜(Session) ๊ธฐ๋ฐ˜ ์‹๋ณ„
์„œ๋ฒ„ ์ธก ์„ธ์…˜์— ์ž„์‹œ ์žฅ๋ฐ”๊ตฌ๋‹ˆ ๋ฐ์ดํ„ฐ๋ฅผ ์ €์žฅํ•˜๋Š” ๋ฐฉ์‹.
๋ธŒ๋ผ์šฐ์ €๋ฅผ ๋‹ซ์œผ๋ฉด ๋ฐ์ดํ„ฐ๊ฐ€ ์‚ฌ๋ผ์ง€๋Š” ๋‹จ์ ์ด ์žˆ์–ด.
PHPNode.jsDjango
โ‘ก ํ† ํฐ(Token) ๊ธฐ๋ฐ˜ ์‹๋ณ„
UUID ๊ฐ™์€ ๊ณ ์œ  ํ† ํฐ์„ ์ƒ์„ฑํ•ด์„œ ์ฟ ํ‚ค๋‚˜ localStorage์— ์ €์žฅ.
๋ธŒ๋ผ์šฐ์ €๋ฅผ ๋‹ซ์•„๋„ ์œ ์ง€๋˜๊ณ , ์„œ๋ฒ„์—์„œ ์ฃผ๋ฌธ ์ •๋ณด์™€ ์—ฐ๊ฒฐ ๊ฐ€๋Šฅ.
JWTUUIDCookie
โ‘ข ์ด๋ฉ”์ผ ๊ธฐ๋ฐ˜ ์‹๋ณ„
์ฒดํฌ์•„์›ƒ ์‹œ ์ด๋ฉ”์ผ ์ฃผ์†Œ๋ฅผ ๋ฐ›์•„์„œ ์ฃผ๋ฌธ์˜ ํ‚ค(key)๋กœ ์‚ฌ์šฉ.
์ฃผ๋ฌธ ์กฐํšŒ, ๋ฐฐ์†ก ์•Œ๋ฆผ ๋“ฑ์— ํ™œ์šฉ ๊ฐ€๋Šฅ.
EmailOrder Token
๐Ÿ’ก ์‹ค๋ฌด ํŒ: ๋Œ€๋ถ€๋ถ„์˜ ํ˜„๋Œ€ ์‡ผํ•‘๋ชฐ์€ ํ† ํฐ + ์ด๋ฉ”์ผ ์กฐํ•ฉ์„ ์‚ฌ์šฉํ•ด.
ํ† ํฐ์œผ๋กœ ์„ธ์…˜์„ ์œ ์ง€ํ•˜๊ณ , ์ด๋ฉ”์ผ๋กœ ์ฃผ๋ฌธ ํ™•์ธ ๋ฐ ์กฐํšŒ๋ฅผ ์ฒ˜๋ฆฌํ•˜๋Š” ๋ฐฉ์‹์ด ๊ฐ€์žฅ ์•ˆ์ •์ ์ด์•ผ.

๐Ÿ“ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์„ค๊ณ„ โ€” ๋น„ํšŒ์› ์ฃผ๋ฌธ์„ ์–ด๋–ป๊ฒŒ ์ €์žฅํ• ๊นŒ?

ํšŒ์› ์ฃผ๋ฌธ๊ณผ ๋น„ํšŒ์› ์ฃผ๋ฌธ์„ ๊ฐ™์€ ํ…Œ์ด๋ธ”์—์„œ ๊ด€๋ฆฌํ•˜๋Š” ๊ฒŒ ์ผ๋ฐ˜์ ์ด์•ผ.
ํ•ต์‹ฌ์€ user_id๋ฅผ NULL ํ—ˆ์šฉ์œผ๋กœ ์„ค๊ณ„ํ•˜๋Š” ๊ฑฐ์•ผ.

-- ์ฃผ๋ฌธ ํ…Œ์ด๋ธ” ์„ค๊ณ„ ์˜ˆ์‹œ (MySQL)
CREATE TABLE orders (
  id            BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
  order_number  VARCHAR(30) NOT NULL UNIQUE,   -- ์ฃผ๋ฌธ๋ฒˆํ˜ธ
  user_id       BIGINT UNSIGNED NULL,           -- ํšŒ์›์ด๋ฉด FK, ๋น„ํšŒ์›์ด๋ฉด NULL
  guest_token   VARCHAR(64) NULL,               -- ๋น„ํšŒ์› ์‹๋ณ„ ํ† ํฐ
  guest_email   VARCHAR(255) NULL,              -- ๋น„ํšŒ์› ์ด๋ฉ”์ผ
  guest_name    VARCHAR(100) NULL,              -- ๋น„ํšŒ์› ์ด๋ฆ„
  guest_phone   VARCHAR(20) NULL,               -- ๋น„ํšŒ์› ์—ฐ๋ฝ์ฒ˜
  status        ENUM('pending','paid','shipping','done','cancel') DEFAULT 'pending',
  total_price   DECIMAL(12,2) NOT NULL,
  created_at    DATETIME DEFAULT CURRENT_TIMESTAMP,
  FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL
);

-- ์ฃผ๋ฌธ ์ƒํ’ˆ ํ…Œ์ด๋ธ”
CREATE TABLE order_items (
  id          BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
  order_id    BIGINT UNSIGNED NOT NULL,
  product_id  BIGINT UNSIGNED NOT NULL,
  quantity    INT NOT NULL,
  price       DECIMAL(12,2) NOT NULL,
  FOREIGN KEY (order_id) REFERENCES orders(id)
);

-- ๋ฐฐ์†ก์ง€ ํ…Œ์ด๋ธ”
CREATE TABLE order_shipping (
  id           BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
  order_id     BIGINT UNSIGNED NOT NULL UNIQUE,
  recipient    VARCHAR(100) NOT NULL,
  address1     VARCHAR(255) NOT NULL,
  address2     VARCHAR(255),
  zip_code     VARCHAR(10) NOT NULL,
  phone        VARCHAR(20) NOT NULL,
  FOREIGN KEY (order_id) REFERENCES orders(id)
);
๐ŸŽฏ ์„ค๊ณ„ ํฌ์ธํŠธ:
โ€ข user_id๋Š” NULL ํ—ˆ์šฉ โ†’ ๋น„ํšŒ์› ์ฃผ๋ฌธ ์‹œ NULL
โ€ข guest_token: UUID๋กœ ์ƒ์„ฑํ•œ ๊ณ ์œ  ํ† ํฐ (์ฃผ๋ฌธ ์กฐํšŒ์— ์‚ฌ์šฉ)
โ€ข guest_email: ์ฃผ๋ฌธ ํ™•์ธ ๋ฉ”์ผ ๋ฐœ์†ก ๋ฐ ์กฐํšŒ ํ‚ค
โ€ข ํšŒ์›/๋น„ํšŒ์› ์ฃผ๋ฌธ์„ ํ•˜๋‚˜์˜ ํ…Œ์ด๋ธ”์—์„œ ํ†ตํ•ฉ ๊ด€๋ฆฌ โ†’ ๊ด€๋ฆฌ ํšจ์œจ โ†‘
๐Ÿ”„ ๊ฒŒ์ŠคํŠธ ์ฒดํฌ์•„์›ƒ ์ „์ฒด ํ”Œ๋กœ์šฐ ๐Ÿ›๏ธ ์ƒํ’ˆ ํŽ˜์ด์ง€ ์žฅ๋ฐ”๊ตฌ๋‹ˆ ๋‹ด๊ธฐ ๐Ÿ›’ ์žฅ๋ฐ”๊ตฌ๋‹ˆ ํ† ํฐ ์ƒ์„ฑ/์ €์žฅ ๐Ÿค” ์ฒดํฌ์•„์›ƒ ๋ฐฉ์‹ ํšŒ์› ๋กœ๊ทธ์ธ OR ๊ฒŒ์ŠคํŠธ๋กœ ๊ณ„์† ํšŒ์› ๊ฒŒ์ŠคํŠธ ๐Ÿ‘ค ํšŒ์› ์ฃผ๋ฌธ user_id ์—ฐ๊ฒฐ ๐Ÿ“ ์ •๋ณด ์ž…๋ ฅ ์ด๋ฉ”์ผยท๋ฐฐ์†ก์ง€ ๐Ÿ’ณ ๊ฒฐ์ œ ์ฒ˜๋ฆฌ PG์‚ฌ ์—ฐ๋™ ๐Ÿ’พ ์ฃผ๋ฌธ DB ์ €์žฅ guest_token + email โœ‰๏ธ ํ™•์ธ ๋ฉ”์ผ ๋ฐœ์†ก ์ฃผ๋ฌธ์กฐํšŒ ๋งํฌ ํฌํ•จ ๐ŸŽ‰ ์ฃผ๋ฌธ ์™„๋ฃŒ! ๐Ÿ” ๊ฒŒ์ŠคํŠธ ํ† ํฐ ์ฒ˜๋ฆฌ โ€ข UUID v4๋กœ ๊ณ ์œ  ํ† ํฐ ์ƒ์„ฑ โ€ข ์ฟ ํ‚ค์— 30์ผ ์ €์žฅ (httpOnly) โ€ข ์žฅ๋ฐ”๊ตฌ๋‹ˆ โ†” ํ† ํฐ ์—ฐ๊ฒฐ โ€ข ์ฃผ๋ฌธ ์™„๋ฃŒ ์‹œ DB์— ์ €์žฅ โ€ข ์ฃผ๋ฌธ ์กฐํšŒ ์‹œ ํ† ํฐ+์ด๋ฉ”์ผ ๊ฒ€์ฆ ๐Ÿ“ฆ ๋น„ํšŒ์› ์ฃผ๋ฌธ ํŠน์ง• โ€ข user_id = NULL โ€ข guest_token์œผ๋กœ ์‹๋ณ„ โ€ข ์ฃผ๋ฌธ ํ›„ ํšŒ์›๊ฐ€์ž… ์œ ๋„ ๊ฐ€๋Šฅ โ€ข ์ด๋ฉ”์ผ๋กœ ์ฃผ๋ฌธ ์ด๋ ฅ ์กฐํšŒ

โš™๏ธ ๋ฐฑ์—”๋“œ ๊ตฌํ˜„ โ€” Node.js(Express) ๊ธฐ์ค€ ์‹ค์ „ ์ฝ”๋“œ

์ž, ์ด์ œ ์ง„์งœ ์ฝ”๋“œ ๋“ค์–ด๊ฐ€๋ณด์ž! ๐Ÿ”ฅ
Node.js + Express ๊ธฐ์ค€์œผ๋กœ ์„ค๋ช…ํ• ๊ฒŒ. ๋‹ค๋ฅธ ์–ธ์–ด๋„ ๋กœ์ง์€ ๋˜‘๊ฐ™์œผ๋‹ˆ๊นŒ ์ฐธ๊ณ ํ•ด๋ด.

1๏ธโƒฃ ๊ฒŒ์ŠคํŠธ ํ† ํฐ ์ƒ์„ฑ ๋ฐ ์žฅ๋ฐ”๊ตฌ๋‹ˆ ๊ด€๋ฆฌ

// utils/guestToken.js
const { v4: uuidv4 } = require('uuid');

// ๊ฒŒ์ŠคํŠธ ํ† ํฐ ์ƒ์„ฑ ๋˜๋Š” ๊ธฐ์กด ํ† ํฐ ๋ฐ˜ํ™˜
function getOrCreateGuestToken(req, res) {
  let token = req.cookies.guest_token;
  
  if (!token) {
    token = uuidv4(); // ์˜ˆ: "550e8400-e29b-41d4-a716-446655440000"
    // httpOnly ์ฟ ํ‚ค๋กœ ์„ค์ • (JS์—์„œ ์ ‘๊ทผ ๋ถˆ๊ฐ€ โ†’ ๋ณด์•ˆ ๊ฐ•ํ™”)
    res.cookie('guest_token', token, {
      maxAge: 30 * 24 * 60 * 60 * 1000, // 30์ผ
      httpOnly: true,
      secure: process.env.NODE_ENV === 'production', // HTTPS์—์„œ๋งŒ
      sameSite: 'lax'
    });
  }
  
  return token;
}

module.exports = { getOrCreateGuestToken };
// routes/cart.js
const express = require('express');
const router = express.Router();
const { getOrCreateGuestToken } = require('../utils/guestToken');
const db = require('../db');

// ์žฅ๋ฐ”๊ตฌ๋‹ˆ์— ์ƒํ’ˆ ์ถ”๊ฐ€
router.post('/add', async (req, res) => {
  const { product_id, quantity } = req.body;
  
  // ๋กœ๊ทธ์ธ ์—ฌ๋ถ€ ํ™•์ธ
  const userId = req.session?.user?.id || null;
  const guestToken = userId ? null : getOrCreateGuestToken(req, res);
  
  // ์žฅ๋ฐ”๊ตฌ๋‹ˆ ํ…Œ์ด๋ธ”์— ์ €์žฅ
  await db.query(
    `INSERT INTO cart_items (user_id, guest_token, product_id, quantity)
     VALUES (?, ?, ?, ?)
     ON DUPLICATE KEY UPDATE quantity = quantity + ?`,
    [userId, guestToken, product_id, quantity, quantity]
  );
  
  res.json({ success: true, message: '์žฅ๋ฐ”๊ตฌ๋‹ˆ์— ์ถ”๊ฐ€๋˜์—ˆ์Šต๋‹ˆ๋‹ค.' });
});

// ์žฅ๋ฐ”๊ตฌ๋‹ˆ ์กฐํšŒ
router.get('/', async (req, res) => {
  const userId = req.session?.user?.id || null;
  const guestToken = req.cookies.guest_token || null;
  
  let items;
  if (userId) {
    items = await db.query(
      `SELECT ci.*, p.name, p.price, p.image 
       FROM cart_items ci JOIN products p ON ci.product_id = p.id
       WHERE ci.user_id = ?`, [userId]
    );
  } else if (guestToken) {
    items = await db.query(
      `SELECT ci.*, p.name, p.price, p.image 
       FROM cart_items ci JOIN products p ON ci.product_id = p.id
       WHERE ci.guest_token = ?`, [guestToken]
    );
  } else {
    items = [];
  }
  
  res.json({ items });
});

2๏ธโƒฃ ๊ฒŒ์ŠคํŠธ ์ฒดํฌ์•„์›ƒ โ€” ์ฃผ๋ฌธ ์ƒ์„ฑ API

// routes/checkout.js
const express = require('express');
const router = express.Router();
const { v4: uuidv4 } = require('uuid');
const db = require('../db');
const mailer = require('../utils/mailer');

// ๊ฒŒ์ŠคํŠธ ์ฃผ๋ฌธ ์ƒ์„ฑ
router.post('/guest', async (req, res) => {
  const {
    guest_email,
    guest_name,
    guest_phone,
    recipient,
    address1,
    address2,
    zip_code,
    shipping_phone,
    payment_method
  } = req.body;

  // ์ž…๋ ฅ๊ฐ’ ๊ฒ€์ฆ
  if (!guest_email || !guest_name || !address1) {
    return res.status(400).json({ error: 'ํ•„์ˆ˜ ์ •๋ณด๋ฅผ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”.' });
  }

  const guestToken = req.cookies.guest_token;
  if (!guestToken) {
    return res.status(400).json({ error: '์žฅ๋ฐ”๊ตฌ๋‹ˆ ์ •๋ณด๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.' });
  }

  // ์žฅ๋ฐ”๊ตฌ๋‹ˆ ์กฐํšŒ
  const cartItems = await db.query(
    `SELECT ci.quantity, p.price, p.id as product_id, p.stock
     FROM cart_items ci JOIN products p ON ci.product_id = p.id
     WHERE ci.guest_token = ?`, [guestToken]
  );

  if (!cartItems.length) {
    return res.status(400).json({ error: '์žฅ๋ฐ”๊ตฌ๋‹ˆ๊ฐ€ ๋น„์–ด์žˆ์Šต๋‹ˆ๋‹ค.' });
  }

  // ์žฌ๊ณ  ํ™•์ธ
  for (const item of cartItems) {
    if (item.stock < item.quantity) {
      return res.status(400).json({ error: `์žฌ๊ณ ๊ฐ€ ๋ถ€์กฑํ•œ ์ƒํ’ˆ์ด ์žˆ์Šต๋‹ˆ๋‹ค.` });
    }
  }

  // ์ด ๊ธˆ์•ก ๊ณ„์‚ฐ
  const totalPrice = cartItems.reduce(
    (sum, item) => sum + (item.price * item.quantity), 0
  );

  // ์ฃผ๋ฌธ๋ฒˆํ˜ธ ์ƒ์„ฑ (์˜ˆ: ORD-20240115-A3F2)
  const orderNumber = `ORD-${Date.now()}-${Math.random().toString(36).substr(2,4).toUpperCase()}`;
  
  // ์ฃผ๋ฌธ ๊ณ ์œ  ์กฐํšŒ ํ† ํฐ (์ด๋ฉ”์ผ์— ํฌํ•จ๋  ๋งํฌ์šฉ)
  const orderToken = uuidv4();

  // ํŠธ๋žœ์žญ์…˜ ์‹œ์ž‘
  const conn = await db.getConnection();
  await conn.beginTransaction();

  try {
    // 1. ์ฃผ๋ฌธ ์ƒ์„ฑ
    const [orderResult] = await conn.query(
      `INSERT INTO orders 
       (order_number, user_id, guest_token, guest_email, guest_name, guest_phone, total_price, order_token)
       VALUES (?, NULL, ?, ?, ?, ?, ?, ?)`,
      [orderNumber, guestToken, guest_email, guest_name, guest_phone, totalPrice, orderToken]
    );
    const orderId = orderResult.insertId;

    // 2. ์ฃผ๋ฌธ ์ƒํ’ˆ ์ €์žฅ
    for (const item of cartItems) {
      await conn.query(
        `INSERT INTO order_items (order_id, product_id, quantity, price)
         VALUES (?, ?, ?, ?)`,
        [orderId, item.product_id, item.quantity, item.price]
      );
      // ์žฌ๊ณ  ์ฐจ๊ฐ
      await conn.query(
        `UPDATE products SET stock = stock - ? WHERE id = ?`,
        [item.quantity, item.product_id]
      );
    }

    // 3. ๋ฐฐ์†ก์ง€ ์ €์žฅ
    await conn.query(
      `INSERT INTO order_shipping (order_id, recipient, address1, address2, zip_code, phone)
       VALUES (?, ?, ?, ?, ?, ?)`,
      [orderId, recipient, address1, address2, zip_code, shipping_phone]
    );

    // 4. ์žฅ๋ฐ”๊ตฌ๋‹ˆ ๋น„์šฐ๊ธฐ
    await conn.query(
      `DELETE FROM cart_items WHERE guest_token = ?`, [guestToken]
    );

    await conn.commit();

    // 5. ์ฃผ๋ฌธ ํ™•์ธ ์ด๋ฉ”์ผ ๋ฐœ์†ก
    const orderViewUrl = `${process.env.BASE_URL}/orders/guest?token=${orderToken}&email=${guest_email}`;
    await mailer.sendOrderConfirmation({
      to: guest_email,
      name: guest_name,
      orderNumber,
      totalPrice,
      orderViewUrl
    });

    res.json({
      success: true,
      orderNumber,
      message: '์ฃผ๋ฌธ์ด ์™„๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ํ™•์ธ ์ด๋ฉ”์ผ์„ ๋ฐœ์†กํ–ˆ์Šต๋‹ˆ๋‹ค.'
    });

  } catch (err) {
    await conn.rollback();
    console.error('์ฃผ๋ฌธ ์ƒ์„ฑ ์˜ค๋ฅ˜:', err);
    res.status(500).json({ error: '์ฃผ๋ฌธ ์ฒ˜๋ฆฌ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.' });
  } finally {
    conn.release();
  }
});
โš ๏ธ ํŠธ๋žœ์žญ์…˜ ์ฒ˜๋ฆฌ ํ•„์ˆ˜!
์ฃผ๋ฌธ ์ƒ์„ฑ, ์žฌ๊ณ  ์ฐจ๊ฐ, ๋ฐฐ์†ก์ง€ ์ €์žฅ์€ ๋ฐ˜๋“œ์‹œ ํ•˜๋‚˜์˜ ํŠธ๋žœ์žญ์…˜์œผ๋กœ ๋ฌถ์–ด์•ผ ํ•ด.
์ค‘๊ฐ„์— ์˜ค๋ฅ˜๊ฐ€ ๋‚˜๋ฉด ์ „์ฒด ๋กค๋ฐฑํ•ด์„œ ๋ฐ์ดํ„ฐ ๋ถˆ์ผ์น˜๋ฅผ ๋ฐฉ์ง€ํ•ด์•ผ ํ•˜๊ฑฐ๋“ .

3๏ธโƒฃ ๋น„ํšŒ์› ์ฃผ๋ฌธ ์กฐํšŒ API

// routes/orders.js

// ๋น„ํšŒ์› ์ฃผ๋ฌธ ์กฐํšŒ (์ด๋ฉ”์ผ + ์ฃผ๋ฌธ ํ† ํฐ์œผ๋กœ ์ธ์ฆ)
router.get('/guest', async (req, res) => {
  const { token, email } = req.query;

  if (!token || !email) {
    return res.status(400).json({ error: '์ฃผ๋ฌธ ์กฐํšŒ ์ •๋ณด๊ฐ€ ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์Šต๋‹ˆ๋‹ค.' });
  }

  // ์ด๋ฉ”์ผ + ํ† ํฐ ๋™์‹œ ๊ฒ€์ฆ (๋‘˜ ๋‹ค ๋งž์•„์•ผ ์กฐํšŒ ๊ฐ€๋Šฅ)
  const [order] = await db.query(
    `SELECT o.*, os.recipient, os.address1, os.address2, os.zip_code
     FROM orders o
     LEFT JOIN order_shipping os ON o.id = os.order_id
     WHERE o.order_token = ? AND o.guest_email = ?`,
    [token, email]
  );

  if (!order) {
    return res.status(404).json({ error: '์ฃผ๋ฌธ์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.' });
  }

  // ์ฃผ๋ฌธ ์ƒํ’ˆ ์กฐํšŒ
  const items = await db.query(
    `SELECT oi.*, p.name, p.image
     FROM order_items oi JOIN products p ON oi.product_id = p.id
     WHERE oi.order_id = ?`,
    [order.id]
  );

  res.json({ order, items });
});
๐Ÿ’ก ๋ณด์•ˆ ํฌ์ธํŠธ: ์ฃผ๋ฌธ ์กฐํšŒ ์‹œ order_token๋งŒ์œผ๋กœ๋Š” ์กฐํšŒ ๋ถˆ๊ฐ€๋Šฅํ•˜๊ฒŒ ํ•ด์•ผ ํ•ด.
๋ฐ˜๋“œ์‹œ ์ด๋ฉ”์ผ + ํ† ํฐ ๋‘ ๊ฐ€์ง€๋ฅผ ๋™์‹œ์— ๊ฒ€์ฆํ•ด์•ผ ๋‹ค๋ฅธ ์‚ฌ๋žŒ์ด ํ† ํฐ์„ ์ถ”์ธกํ•ด์„œ ์กฐํšŒํ•˜๋Š” ๊ฑธ ๋ง‰์„ ์ˆ˜ ์žˆ์–ด.

๐ŸŽจ ํ”„๋ก ํŠธ์—”๋“œ ๊ตฌํ˜„ โ€” ๊ฒŒ์ŠคํŠธ ์ฒดํฌ์•„์›ƒ UI/UX

๋ฐฑ์—”๋“œ๋งŒ ์ž˜ ๋งŒ๋“ค๋ฉด ๋ญํ•ด, ํ”„๋ก ํŠธ๋„ ์ž˜ ๋งŒ๋“ค์–ด์•ผ์ง€! ๐Ÿ˜„
๊ฒŒ์ŠคํŠธ ์ฒดํฌ์•„์›ƒ UI์—์„œ ๊ฐ€์žฅ ์ค‘์š”ํ•œ ๊ฑด "๋งˆ์ฐฐ์„ ์ตœ์†Œํ™”"ํ•˜๋Š” ๊ฑฐ์•ผ.

๐Ÿ–ฅ๏ธ ์ฒดํฌ์•„์›ƒ ๋ฐฉ์‹ ์„ ํƒ ํ™”๋ฉด (React ์˜ˆ์‹œ)

// components/CheckoutOptions.jsx
import React, { useState } from 'react';

function CheckoutOptions({ onSelectMethod }) {
  const [email, setEmail] = useState('');

  return (
    <div className="checkout-options">
      <h2>์ฃผ๋ฌธ ๋ฐฉ์‹์„ ์„ ํƒํ•ด์ฃผ์„ธ์š”</h2>
      
      {/* ํšŒ์› ๋กœ๊ทธ์ธ ์˜ต์…˜ */}
      <div className="option-card">
        <h3>๐Ÿ‘ค ํšŒ์›์œผ๋กœ ์ฃผ๋ฌธ</h3>
        <p>๋กœ๊ทธ์ธํ•˜๋ฉด ์ฃผ๋ฌธ ์ด๋ ฅ ๊ด€๋ฆฌ, ํฌ์ธํŠธ ์ ๋ฆฝ์ด ๊ฐ€๋Šฅํ•ด์š”</p>
        <button onClick={() => onSelectMethod('login')}>
          ๋กœ๊ทธ์ธํ•˜๊ธฐ
        </button>
      </div>

      {/* ๊ฒŒ์ŠคํŠธ ์ฃผ๋ฌธ ์˜ต์…˜ */}
      <div className="option-card guest">
        <h3>๐Ÿš€ ๋น„ํšŒ์›์œผ๋กœ ๋น ๋ฅด๊ฒŒ ์ฃผ๋ฌธ</h3>
        <p>ํšŒ์›๊ฐ€์ž… ์—†์ด ์ด๋ฉ”์ผ๋งŒ์œผ๋กœ ์ฃผ๋ฌธํ•  ์ˆ˜ ์žˆ์–ด์š”</p>
        <input
          type="email"
          placeholder="์ด๋ฉ”์ผ ์ฃผ์†Œ ์ž…๋ ฅ"
          value={email}
          onChange={(e) => setEmail(e.target.value)}
        />
        <button 
          onClick={() => onSelectMethod('guest', email)}
          disabled={!email.includes('@')}
        >
          ๋น„ํšŒ์›์œผ๋กœ ๊ณ„์†ํ•˜๊ธฐ โ†’
        </button>
      </div>

      {/* ์ฃผ๋ฌธ ํ›„ ํšŒ์›๊ฐ€์ž… ์œ ๋„ ๋ฌธ๊ตฌ */}
      <p className="hint">
        ๐Ÿ’ก ์ฃผ๋ฌธ ์™„๋ฃŒ ํ›„ ํšŒ์›๊ฐ€์ž…ํ•˜๋ฉด ์ด ์ฃผ๋ฌธ๋„ ์ด๋ ฅ์— ์—ฐ๊ฒฐ๋ผ์š”!
      </p>
    </div>
  );
}

๐Ÿ“‹ ๊ฒŒ์ŠคํŠธ ์ •๋ณด ์ž…๋ ฅ ํผ

// components/GuestCheckoutForm.jsx
function GuestCheckoutForm({ guestEmail, onSubmit }) {
  const [form, setForm] = useState({
    guest_name: '',
    guest_phone: '',
    recipient: '',
    address1: '',
    address2: '',
    zip_code: '',
    shipping_phone: ''
  });

  const handleSubmit = async (e) => {
    e.preventDefault();
    
    // ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ
    if (!form.guest_name || !form.guest_phone) {
      alert('์ด๋ฆ„๊ณผ ์—ฐ๋ฝ์ฒ˜๋ฅผ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”.');
      return;
    }
    if (!form.address1 || !form.zip_code) {
      alert('๋ฐฐ์†ก์ง€๋ฅผ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”.');
      return;
    }

    try {
      const response = await fetch('/api/checkout/guest', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          guest_email: guestEmail,
          ...form
        })
      });

      const data = await response.json();
      
      if (data.success) {
        // ์ฃผ๋ฌธ ์™„๋ฃŒ ํŽ˜์ด์ง€๋กœ ์ด๋™
        window.location.href = `/order-complete?orderNumber=${data.orderNumber}`;
      }
    } catch (err) {
      alert('์ฃผ๋ฌธ ์ฒ˜๋ฆฌ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.');
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <h3>์ฃผ๋ฌธ์ž ์ •๋ณด</h3>
      <input placeholder="์ด๋ฆ„" value={form.guest_name}
        onChange={e => setForm({...form, guest_name: e.target.value})} />
      <input placeholder="์—ฐ๋ฝ์ฒ˜" value={form.guest_phone}
        onChange={e => setForm({...form, guest_phone: e.target.value})} />
      
      <h3>๋ฐฐ์†ก์ง€ ์ •๋ณด</h3>
      <input placeholder="์šฐํŽธ๋ฒˆํ˜ธ" value={form.zip_code}
        onChange={e => setForm({...form, zip_code: e.target.value})} />
      <input placeholder="๊ธฐ๋ณธ ์ฃผ์†Œ" value={form.address1}
        onChange={e => setForm({...form, address1: e.target.value})} />
      <input placeholder="์ƒ์„ธ ์ฃผ์†Œ" value={form.address2}
        onChange={e => setForm({...form, address2: e.target.value})} />
      
      <button type="submit">๊ฒฐ์ œํ•˜๊ธฐ ๐Ÿ’ณ</button>
    </form>
  );
}
๐ŸŽฏ UX ์ตœ์ ํ™” ํŒ:
โ€ข ์ฃผ์†Œ ์ž…๋ ฅ์€ ์นด์นด์˜ค ์šฐํŽธ๋ฒˆํ˜ธ API ์—ฐ๋™์œผ๋กœ ์ž๋™ํ™”
โ€ข ์—ฐ๋ฝ์ฒ˜ ์ž…๋ ฅ ์‹œ ์ž๋™ ํ•˜์ดํ”ˆ ํฌ๋งทํŒ… (010-1234-5678)
โ€ข ํ•„์ˆ˜ ํ•ญ๋ชฉ์€ * ํ‘œ์‹œ + ์‹ค์‹œ๊ฐ„ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ
โ€ข ๋ชจ๋ฐ”์ผ์—์„œ๋Š” ์ ์ ˆํ•œ inputmode ์†์„ฑ ์„ค์ • (์ˆซ์ž ํ‚คํŒจ๋“œ ๋“ฑ)

๐Ÿ” ๋ณด์•ˆ ์ฒ˜๋ฆฌ โ€” ๋น„ํšŒ์› ์ฃผ๋ฌธ์˜ ์ทจ์•ฝ์ ๊ณผ ๋Œ€์‘

๊ฒŒ์ŠคํŠธ ์ฒดํฌ์•„์›ƒ์€ ํŽธ๋ฆฌํ•˜์ง€๋งŒ, ๋ณด์•ˆ ํ—ˆ์ ์ด ์ƒ๊ธฐ๊ธฐ ์‰ฌ์›Œ.
์–ด๋–ค ๊ณต๊ฒฉ ๋ฒกํ„ฐ๊ฐ€ ์žˆ๊ณ  ์–ด๋–ป๊ฒŒ ๋ง‰์•„์•ผ ํ•˜๋Š”์ง€ ์•Œ์•„๋ณด์ž.

๐Ÿ˜ˆ ์ฃผ์š” ์ทจ์•ฝ์ 

๐Ÿ”ด ํ† ํฐ ์ถ”์ธก ๊ณต๊ฒฉ
๐Ÿ”ด ์ฃผ๋ฌธ ์กฐํšŒ ๋ฌด์ฐจ๋ณ„ ๋Œ€์ž…
๐Ÿ”ด CSRF ๊ณต๊ฒฉ
๐Ÿ”ด ์ด๋ฉ”์ผ ์ŠคํŒธ ์ฃผ๋ฌธ
๐Ÿ”ด ์žฌ๊ณ  ์†Œ์ง„ ๊ณต๊ฒฉ
๐Ÿ›ก๏ธ ๋Œ€์‘ ๋ฐฉ๋ฒ•

โœ… UUID v4 (128bit ๋žœ๋ค)
โœ… Rate Limiting ์ ์šฉ
โœ… CSRF ํ† ํฐ ์‚ฌ์šฉ
โœ… reCAPTCHA ์—ฐ๋™
โœ… ๊ฒฐ์ œ ์™„๋ฃŒ ํ›„ ์žฌ๊ณ  ์ฐจ๊ฐ

๐Ÿ›ก๏ธ Rate Limiting ์ ์šฉ (Express)

// middleware/rateLimiter.js
const rateLimit = require('express-rate-limit');

// ์ฃผ๋ฌธ API Rate Limiting
const orderLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15๋ถ„
  max: 5,                    // ์ตœ๋Œ€ 5ํšŒ ์ฃผ๋ฌธ ์‹œ๋„
  message: { error: '๋„ˆ๋ฌด ๋งŽ์€ ์ฃผ๋ฌธ ์‹œ๋„์ž…๋‹ˆ๋‹ค. ์ž ์‹œ ํ›„ ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”.' },
  standardHeaders: true,
  legacyHeaders: false,
});

// ์ฃผ๋ฌธ ์กฐํšŒ Rate Limiting
const orderViewLimiter = rateLimit({
  windowMs: 5 * 60 * 1000,  // 5๋ถ„
  max: 20,                   // ์ตœ๋Œ€ 20ํšŒ ์กฐํšŒ
  message: { error: '์กฐํšŒ ํšŸ์ˆ˜๋ฅผ ์ดˆ๊ณผํ–ˆ์Šต๋‹ˆ๋‹ค.' }
});

module.exports = { orderLimiter, orderViewLimiter };

// app.js์—์„œ ์ ์šฉ
app.use('/api/checkout/guest', orderLimiter);
app.use('/api/orders/guest', orderViewLimiter);

๐Ÿ”’ CSRF ๋ณดํ˜ธ

// CSRF ํ† ํฐ ๋ฏธ๋“ค์›จ์–ด ์ ์šฉ (csurf ํŒจํ‚ค์ง€)
const csrf = require('csurf');
const csrfProtection = csrf({ cookie: true });

// ์ฒดํฌ์•„์›ƒ ํผ ๋ Œ๋”๋ง ์‹œ CSRF ํ† ํฐ ์ „๋‹ฌ
app.get('/checkout', csrfProtection, (req, res) => {
  res.json({ csrfToken: req.csrfToken() });
});

// ์ฃผ๋ฌธ API์— CSRF ๊ฒ€์ฆ ์ ์šฉ
app.post('/api/checkout/guest', csrfProtection, orderLimiter, async (req, res) => {
  // ... ์ฃผ๋ฌธ ์ฒ˜๋ฆฌ ๋กœ์ง
});
โš ๏ธ ์žฌ๊ณ  ์ฒ˜๋ฆฌ ์ฃผ์˜์‚ฌํ•ญ:
๊ฒฐ์ œ ์™„๋ฃŒ ์ „์— ์žฌ๊ณ ๋ฅผ ์ฐจ๊ฐํ•˜๋ฉด ๊ฒฐ์ œ ์‹คํŒจ ์‹œ ์žฌ๊ณ ๊ฐ€ ์ž˜๋ชป ๊ด€๋ฆฌ๋  ์ˆ˜ ์žˆ์–ด.
PG์‚ฌ ๊ฒฐ์ œ ์™„๋ฃŒ ์›นํ›…(Webhook)์„ ๋ฐ›์€ ํ›„์— ์žฌ๊ณ ๋ฅผ ์ฐจ๊ฐํ•˜๋Š” ๊ฒŒ ์•ˆ์ „ํ•ด.
๋˜๋Š” ์ฃผ๋ฌธ ์ƒ์„ฑ ์‹œ ์žฌ๊ณ ๋ฅผ ์ž„์‹œ ์˜ˆ์•ฝ(hold)ํ•˜๊ณ , ๊ฒฐ์ œ ์™„๋ฃŒ ์‹œ ํ™•์ •ํ•˜๋Š” ๋ฐฉ์‹๋„ ์ข‹์•„.

๐Ÿ“ง ์ด๋ฉ”์ผ ์ธ์ฆ์œผ๋กœ ์ŠคํŒธ ์ฃผ๋ฌธ ๋ฐฉ์ง€

// ์ด๋ฉ”์ผ ์ธ์ฆ ์ฝ”๋“œ ๋ฐœ์†ก (์„ ํƒ์  ์ ์šฉ)
router.post('/verify-email', async (req, res) => {
  const { email } = req.body;
  
  // 6์ž๋ฆฌ ์ธ์ฆ ์ฝ”๋“œ ์ƒ์„ฑ
  const verifyCode = Math.floor(100000 + Math.random() * 900000).toString();
  
  // Redis์— 5๋ถ„๊ฐ„ ์ €์žฅ
  await redis.setex(`email_verify:${email}`, 300, verifyCode);
  
  // ์ธ์ฆ ๋ฉ”์ผ ๋ฐœ์†ก
  await mailer.sendVerifyCode({ to: email, code: verifyCode });
  
  res.json({ success: true, message: '์ธ์ฆ ์ฝ”๋“œ๋ฅผ ๋ฐœ์†กํ–ˆ์Šต๋‹ˆ๋‹ค.' });
});

// ์ธ์ฆ ์ฝ”๋“œ ํ™•์ธ
router.post('/confirm-email', async (req, res) => {
  const { email, code } = req.body;
  
  const savedCode = await redis.get(`email_verify:${email}`);
  
  if (!savedCode || savedCode !== code) {
    return res.status(400).json({ error: '์ธ์ฆ ์ฝ”๋“œ๊ฐ€ ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์Šต๋‹ˆ๋‹ค.' });
  }
  
  // ์ธ์ฆ ์™„๋ฃŒ ํ‘œ์‹œ
  await redis.setex(`email_verified:${email}`, 3600, '1'); // 1์‹œ๊ฐ„ ์œ ํšจ
  await redis.del(`email_verify:${email}`);
  
  res.json({ success: true });
});

๐Ÿ”„ ์ฃผ๋ฌธ ํ›„ ํšŒ์› ์ „ํ™˜ โ€” ๋น„ํšŒ์›์„ ํšŒ์›์œผ๋กœ ๋งŒ๋“œ๋Š” ์ „๋žต

๊ฒŒ์ŠคํŠธ ์ฒดํฌ์•„์›ƒ์˜ ์ง„์งœ ๋ชฉ์ ์€ ๋‹จ์ˆœํžˆ ์ฃผ๋ฌธ์„ ์™„๋ฃŒ์‹œํ‚ค๋Š” ๊ฒŒ ์•„๋‹ˆ์•ผ.
์ฃผ๋ฌธ ์™„๋ฃŒ ํ›„ ์ž์—ฐ์Šค๋Ÿฝ๊ฒŒ ํšŒ์›๊ฐ€์ž…์„ ์œ ๋„ํ•ด์„œ ์ถฉ์„ฑ ๊ณ ๊ฐ์œผ๋กœ ๋งŒ๋“œ๋Š” ๊ฒŒ ์ตœ์ข… ๋ชฉํ‘œ์•ผ! ๐ŸŽฏ

1
์ฃผ๋ฌธ ์™„๋ฃŒ ํŽ˜์ด์ง€์—์„œ ๊ฐ€์ž… ์œ ๋„
"์ด ์ด๋ฉ”์ผ๋กœ ๊ฐ€์ž…ํ•˜๋ฉด ์ด ์ฃผ๋ฌธ์ด ์ž๋™์œผ๋กœ ์—ฐ๊ฒฐ๋ผ์š”!" ๋ผ๋Š” ๋ฌธ๊ตฌ์™€ ํ•จ๊ป˜
์›ํด๋ฆญ ํšŒ์›๊ฐ€์ž… ๋ฒ„ํŠผ ์ œ๊ณต. ์ด๋ฏธ ์ด๋ฉ”์ผ์€ ์ž…๋ ฅํ–ˆ์œผ๋‹ˆ ๋น„๋ฐ€๋ฒˆํ˜ธ๋งŒ ์„ค์ •ํ•˜๋ฉด ๋ผ.
2
์ฃผ๋ฌธ ์ด๋ ฅ ์—ฐ๊ฒฐ ๋กœ์ง
๋น„ํšŒ์›์ด ๊ฐ€์ž… ์‹œ guest_email์ด ๊ฐ™์€ ์ฃผ๋ฌธ๋“ค์„ ์ž๋™์œผ๋กœ user_id์— ์—ฐ๊ฒฐ.
3
ํ™•์ธ ์ด๋ฉ”์ผ์— ๊ฐ€์ž… ๋งํฌ ํฌํ•จ
์ฃผ๋ฌธ ํ™•์ธ ์ด๋ฉ”์ผ์— "ํšŒ์›๊ฐ€์ž…ํ•˜๊ณ  ํฌ์ธํŠธ ๋ฐ›๊ธฐ" ๋ฒ„ํŠผ์„ ๋„ฃ์–ด์„œ ์žฌ๋ฐฉ๋ฌธ ์œ ๋„.
// ๋น„ํšŒ์› ์ฃผ๋ฌธ์„ ํšŒ์› ๊ณ„์ •์— ์—ฐ๊ฒฐํ•˜๋Š” ๋กœ์ง
router.post('/link-guest-orders', async (req, res) => {
  const userId = req.session.user.id;
  const userEmail = req.session.user.email;
  
  // ๊ฐ™์€ ์ด๋ฉ”์ผ๋กœ ๋œ ๋น„ํšŒ์› ์ฃผ๋ฌธ์„ ํšŒ์› ๊ณ„์ •์— ์—ฐ๊ฒฐ
  const result = await db.query(
    `UPDATE orders 
     SET user_id = ?, guest_token = NULL
     WHERE guest_email = ? AND user_id IS NULL`,
    [userId, userEmail]
  );
  
  res.json({ 
    success: true, 
    linkedOrders: result.affectedRows,
    message: `${result.affectedRows}๊ฐœ์˜ ์ฃผ๋ฌธ์ด ๊ณ„์ •์— ์—ฐ๊ฒฐ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.`
  });
});
๐Ÿ’ก ์žฌ๋Šฅ๋„ท ๊ฐ™์€ ํ”Œ๋žซํผ์—์„œ๋„ ๋น„์Šทํ•œ ๋ฐฉ์‹์„ ์‚ฌ์šฉํ•ด.
์„œ๋น„์Šค๋ฅผ ์ฒ˜์Œ ์ด์šฉํ•˜๋Š” ์‚ฌ๋žŒ์ด ๋ถ€๋‹ด ์—†์ด ์‹œ์ž‘ํ•  ์ˆ˜ ์žˆ๋„๋ก ์ง„์ž… ์žฅ๋ฒฝ์„ ๋‚ฎ์ถ”๊ณ ,
์ด์šฉ ๊ฒฝํ—˜์ด ์ข‹์œผ๋ฉด ์ž์—ฐ์Šค๋Ÿฝ๊ฒŒ ํšŒ์›์œผ๋กœ ์ „ํ™˜๋˜๋Š” ํ๋ฆ„์ด ํ•ต์‹ฌ์ด์•ผ.

๐Ÿ”€ ์žฅ๋ฐ”๊ตฌ๋‹ˆ ๋ณ‘ํ•ฉ โ€” ๋น„ํšŒ์› โ†’ ํšŒ์› ๋กœ๊ทธ์ธ ์‹œ ์ฒ˜๋ฆฌ

๋น„ํšŒ์›์œผ๋กœ ์žฅ๋ฐ”๊ตฌ๋‹ˆ์— ๋‹ด์•„๋’€๋‹ค๊ฐ€ ๋กœ๊ทธ์ธํ•˜๋ฉด ์–ด๋–ป๊ฒŒ ๋ ๊นŒ?
์ด ์ƒํ™ฉ์„ ์ œ๋Œ€๋กœ ์ฒ˜๋ฆฌ ์•ˆ ํ•˜๋ฉด ์žฅ๋ฐ”๊ตฌ๋‹ˆ๊ฐ€ ๋‚ ์•„๊ฐ€๋Š” ์ตœ์•…์˜ UX๊ฐ€ ๋ฐœ์ƒํ•ด. ๐Ÿ˜ฑ

// ๋กœ๊ทธ์ธ ์„ฑ๊ณต ํ›„ ์žฅ๋ฐ”๊ตฌ๋‹ˆ ๋ณ‘ํ•ฉ ์ฒ˜๋ฆฌ
async function mergeGuestCart(userId, guestToken) {
  if (!guestToken) return;
  
  // ๊ฒŒ์ŠคํŠธ ์žฅ๋ฐ”๊ตฌ๋‹ˆ ์กฐํšŒ
  const guestItems = await db.query(
    `SELECT * FROM cart_items WHERE guest_token = ?`, [guestToken]
  );
  
  for (const item of guestItems) {
    // ํšŒ์› ์žฅ๋ฐ”๊ตฌ๋‹ˆ์— ๊ฐ™์€ ์ƒํ’ˆ์ด ์žˆ๋Š”์ง€ ํ™•์ธ
    const [existing] = await db.query(
      `SELECT * FROM cart_items WHERE user_id = ? AND product_id = ?`,
      [userId, item.product_id]
    );
    
    if (existing) {
      // ์ˆ˜๋Ÿ‰ ํ•ฉ์‚ฐ
      await db.query(
        `UPDATE cart_items SET quantity = quantity + ? 
         WHERE user_id = ? AND product_id = ?`,
        [item.quantity, userId, item.product_id]
      );
    } else {
      // ํšŒ์› ์žฅ๋ฐ”๊ตฌ๋‹ˆ๋กœ ์ด์ „
      await db.query(
        `UPDATE cart_items SET user_id = ?, guest_token = NULL 
         WHERE id = ?`,
        [userId, item.id]
      );
    }
  }
  
  // ๊ฒŒ์ŠคํŠธ ์žฅ๋ฐ”๊ตฌ๋‹ˆ ์ •๋ฆฌ
  await db.query(
    `DELETE FROM cart_items WHERE guest_token = ?`, [guestToken]
  );
}

// ๋กœ๊ทธ์ธ ๋ผ์šฐํ„ฐ์—์„œ ํ˜ธ์ถœ
router.post('/login', async (req, res) => {
  // ... ๋กœ๊ทธ์ธ ์ฒ˜๋ฆฌ ...
  
  // ๋กœ๊ทธ์ธ ์„ฑ๊ณต ํ›„ ์žฅ๋ฐ”๊ตฌ๋‹ˆ ๋ณ‘ํ•ฉ
  const guestToken = req.cookies.guest_token;
  await mergeGuestCart(user.id, guestToken);
  
  // ๊ฒŒ์ŠคํŠธ ์ฟ ํ‚ค ์‚ญ์ œ
  res.clearCookie('guest_token');
  
  res.json({ success: true });
});
๐ŸŽฏ ๋ณ‘ํ•ฉ ์ „๋žต ์„ ํƒ์ง€:
โ€ข ์ˆ˜๋Ÿ‰ ํ•ฉ์‚ฐ: ๊ฐ™์€ ์ƒํ’ˆ์ด๋ฉด ์ˆ˜๋Ÿ‰์„ ๋”ํ•จ (๊ฐ€์žฅ ์ผ๋ฐ˜์ )
โ€ข ๊ฒŒ์ŠคํŠธ ์šฐ์„ : ๊ฒŒ์ŠคํŠธ ์žฅ๋ฐ”๊ตฌ๋‹ˆ๋กœ ๋ฎ์–ด์”€
โ€ข ํšŒ์› ์šฐ์„ : ๊ธฐ์กด ํšŒ์› ์žฅ๋ฐ”๊ตฌ๋‹ˆ ์œ ์ง€
โ€ข ์‚ฌ์šฉ์ž ์„ ํƒ: "๊ธฐ์กด ์žฅ๋ฐ”๊ตฌ๋‹ˆ์™€ ํ•ฉ์น ๊นŒ์š”?" ํŒ์—… ์ œ๊ณต

๋Œ€๋ถ€๋ถ„์˜ ์‡ผํ•‘๋ชฐ์€ ์ˆ˜๋Ÿ‰ ํ•ฉ์‚ฐ ๋ฐฉ์‹์„ ์‚ฌ์šฉํ•ด.

โœ‰๏ธ ์ฃผ๋ฌธ ํ™•์ธ ์ด๋ฉ”์ผ โ€” ๋น„ํšŒ์› ์†Œํ†ต์˜ ํ•ต์‹ฌ

๋น„ํšŒ์›์€ ๋งˆ์ดํŽ˜์ด์ง€๊ฐ€ ์—†์–ด. ๊ทธ๋ž˜์„œ ์ด๋ฉ”์ผ์ด ์œ ์ผํ•œ ์†Œํ†ต ์ฑ„๋„์ด์•ผ.
์ฃผ๋ฌธ ํ™•์ธ ์ด๋ฉ”์ผ์„ ์ž˜ ๋งŒ๋“œ๋Š” ๊ฒŒ ์ •๋ง ์ค‘์š”ํ•ด.

// utils/mailer.js (Nodemailer ์‚ฌ์šฉ)
const nodemailer = require('nodemailer');

const transporter = nodemailer.createTransport({
  service: 'gmail', // ๋˜๋Š” AWS SES, SendGrid ๋“ฑ
  auth: {
    user: process.env.EMAIL_USER,
    pass: process.env.EMAIL_PASS
  }
});

async function sendOrderConfirmation({ to, name, orderNumber, totalPrice, orderViewUrl }) {
  const html = `
    <div style="font-family: sans-serif; max-width: 600px; margin: 0 auto;">
      <h2 style="color: #1a1a2e;">๐ŸŽ‰ ์ฃผ๋ฌธ์ด ์™„๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค!</h2>
      <p>์•ˆ๋…•ํ•˜์„ธ์š”, <strong>${name}</strong>๋‹˜!</p>
      <p>์ฃผ๋ฌธ๋ฒˆํ˜ธ: <strong>${orderNumber}</strong></p>
      <p>๊ฒฐ์ œ๊ธˆ์•ก: <strong>${totalPrice.toLocaleString()}์›</strong></p>
      
      <a href="${orderViewUrl}" 
         style="display:inline-block; background:#0f3460; color:#fff; 
                padding:12px 24px; border-radius:8px; text-decoration:none; margin:16px 0;">
        ๐Ÿ“ฆ ์ฃผ๋ฌธ ์ƒ์„ธ ๋ณด๊ธฐ
      </a>
      
      <hr/>
      <p style="color:#888; font-size:12px;">
        ์ด ๋งํฌ๋Š” ๋ณด์•ˆ์„ ์œ„ํ•ด 30์ผ ํ›„ ๋งŒ๋ฃŒ๋ฉ๋‹ˆ๋‹ค.<br>
        ํšŒ์›๊ฐ€์ž…ํ•˜์‹œ๋ฉด ์–ธ์ œ๋“ ์ง€ ์ฃผ๋ฌธ ์ด๋ ฅ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ์–ด์š”!
      </p>
    </div>
  `;

  await transporter.sendMail({
    from: `"์‡ผํ•‘๋ชฐ" <${process.env.EMAIL_USER}>`,
    to,
    subject: `[์ฃผ๋ฌธ์™„๋ฃŒ] ${orderNumber} ์ฃผ๋ฌธ์ด ์ ‘์ˆ˜๋˜์—ˆ์Šต๋‹ˆ๋‹ค`,
    html
  });
}

module.exports = { sendOrderConfirmation };
๐Ÿ’ก ์ด๋ฉ”์ผ ๋ฐœ์†ก ์„œ๋น„์Šค ์ถ”์ฒœ:
โ€ข SendGrid: ๋ฌด๋ฃŒ 100๊ฑด/์ผ, ๋Œ€์šฉ๋Ÿ‰ ๋ฐœ์†ก์— ์ ํ•ฉ
โ€ข AWS SES: ์ €๋ ดํ•œ ๋น„์šฉ, AWS ์ธํ”„๋ผ ์‚ฌ์šฉ ์‹œ ์ถ”์ฒœ
โ€ข Mailgun: ๊ฐœ๋ฐœ์ž ์นœํ™”์  API
โ€ข Nodemailer + Gmail: ์†Œ๊ทœ๋ชจ ํ”„๋กœ์ ํŠธ์— ์ ํ•ฉ (์ผ์ผ ํ•œ๋„ ์žˆ์Œ)

๐Ÿ› ๏ธ ์ฃผ์š” ํ”Œ๋žซํผ๋ณ„ ๊ฒŒ์ŠคํŠธ ์ฒดํฌ์•„์›ƒ ๊ตฌํ˜„

์ง์ ‘ ๊ฐœ๋ฐœํ•˜๋Š” ๊ฒฝ์šฐ ์™ธ์—๋„, ๊ธฐ์กด ํ”Œ๋žซํผ์„ ์‚ฌ์šฉํ•˜๋Š” ๊ฒฝ์šฐ๋„ ๋งŽ์ง€?
์ฃผ์š” ํ”Œ๋žซํผ์—์„œ ๊ฒŒ์ŠคํŠธ ์ฒดํฌ์•„์›ƒ์„ ์–ด๋–ป๊ฒŒ ์„ค์ •ํ•˜๋Š”์ง€ ์•Œ์•„๋ณด์ž.

๐ŸŸฃ WooCommerce (WordPress)
WooCommerce โ†’ ์„ค์ • โ†’ ๊ณ„์ • ๋ฐ ๊ฐœ์ธ์ •๋ณด์—์„œ
"๊ฒŒ์ŠคํŠธ ์ฒดํฌ์•„์›ƒ ํ—ˆ์šฉ" ์˜ต์…˜ ํ™œ์„ฑํ™”.
์ถ”๊ฐ€ ์ปค์Šคํ„ฐ๋งˆ์ด์ง•์€ woocommerce_checkout_fields ํ•„ํ„ฐ ํ›… ์‚ฌ์šฉ.
๐ŸŸข Shopify
๊ธฐ๋ณธ์ ์œผ๋กœ ๊ฒŒ์ŠคํŠธ ์ฒดํฌ์•„์›ƒ ์ง€์›.
Settings โ†’ Checkout โ†’ Customer accounts์—์„œ
"Accounts are optional" ์„ ํƒํ•˜๋ฉด ๊ฒŒ์ŠคํŠธ ์ฃผ๋ฌธ ๊ฐ€๋Šฅ.
๐Ÿ”ต Magento 2
Stores โ†’ Configuration โ†’ Sales โ†’ Checkout โ†’ Checkout Options
์—์„œ
"Allow Guest Checkout" = Yes ์„ค์ •.
๐ŸŸก NestJS + TypeORM (์ปค์Šคํ…€ ๊ฐœ๋ฐœ)
@IsOptional() ๋ฐ์ฝ”๋ ˆ์ดํ„ฐ๋กœ user_id ํ•„๋“œ๋ฅผ ์„ ํƒ์ ์œผ๋กœ ์ฒ˜๋ฆฌ.
Guard์—์„œ @Public() ๋ฐ์ฝ”๋ ˆ์ดํ„ฐ๋กœ ์ธ์ฆ ์šฐํšŒ ์ฒ˜๋ฆฌ.

๐Ÿ Django ๊ธฐ๋ฐ˜ ๊ตฌํ˜„ ์˜ˆ์‹œ

# views.py - Django ๊ฒŒ์ŠคํŠธ ์ฃผ๋ฌธ ๋ทฐ
from django.views import View
from django.http import JsonResponse
import uuid
import json

class GuestCheckoutView(View):
    def post(self, request):
        data = json.loads(request.body)
        
        # ๊ฒŒ์ŠคํŠธ ํ† ํฐ ํ™•์ธ
        guest_token = request.COOKIES.get('guest_token')
        if not guest_token:
            return JsonResponse({'error': '์žฅ๋ฐ”๊ตฌ๋‹ˆ ์ •๋ณด๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.'}, status=400)
        
        # ์ฃผ๋ฌธ ์ƒ์„ฑ
        order = Order.objects.create(
            user=None,  # ๋น„ํšŒ์›
            guest_token=guest_token,
            guest_email=data.get('guest_email'),
            guest_name=data.get('guest_name'),
            guest_phone=data.get('guest_phone'),
            order_token=str(uuid.uuid4()),
            total_price=calculate_total(guest_token)
        )
        
        # ์žฅ๋ฐ”๊ตฌ๋‹ˆ โ†’ ์ฃผ๋ฌธ ์ƒํ’ˆ ์ด์ „
        cart_items = CartItem.objects.filter(guest_token=guest_token)
        for item in cart_items:
            OrderItem.objects.create(
                order=order,
                product=item.product,
                quantity=item.quantity,
                price=item.product.price
            )
        
        # ์žฅ๋ฐ”๊ตฌ๋‹ˆ ๋น„์šฐ๊ธฐ
        cart_items.delete()
        
        # ํ™•์ธ ์ด๋ฉ”์ผ ๋ฐœ์†ก (Celery ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ ๊ถŒ์žฅ)
        send_order_confirmation.delay(order.id)
        
        return JsonResponse({
            'success': True,
            'order_number': order.order_number
        })

โšก ์„ฑ๋Šฅ ์ตœ์ ํ™” โ€” ๊ฒŒ์ŠคํŠธ ์ฒดํฌ์•„์›ƒ์„ ๋น ๋ฅด๊ฒŒ

๊ฒŒ์ŠคํŠธ ์ฒดํฌ์•„์›ƒ์˜ ํ•ต์‹ฌ์€ ์†๋„์•ผ.
๋А๋ฆฌ๋ฉด ์ดํƒˆํ•˜๊ฑฐ๋“ . ์ตœ์ ํ™” ํฌ์ธํŠธ๋ฅผ ์งš์–ด๋ณด์ž.

1
Redis๋กœ ์žฅ๋ฐ”๊ตฌ๋‹ˆ ์บ์‹ฑ
DB ๋Œ€์‹  Redis์— ์žฅ๋ฐ”๊ตฌ๋‹ˆ ๋ฐ์ดํ„ฐ๋ฅผ ์ €์žฅํ•˜๋ฉด ์ฝ๊ธฐ ์†๋„๊ฐ€ 10๋ฐฐ ์ด์ƒ ๋นจ๋ผ์ ธ.
cart:{guest_token} ํ‚ค๋กœ JSON ์ €์žฅ, TTL 30์ผ ์„ค์ •.
2
์ด๋ฉ”์ผ ๋ฐœ์†ก ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ
์ด๋ฉ”์ผ ๋ฐœ์†ก์„ ๋™๊ธฐ๋กœ ์ฒ˜๋ฆฌํ•˜๋ฉด ์‘๋‹ต์ด ๋А๋ ค์ ธ.
๋ฉ”์‹œ์ง€ ํ(RabbitMQ, Bull, Celery)๋ฅผ ์‚ฌ์šฉํ•ด์„œ ๋น„๋™๊ธฐ๋กœ ์ฒ˜๋ฆฌํ•ด.
3
๊ฒฐ์ œ ์›นํ›… ์ฒ˜๋ฆฌ ์ตœ์ ํ™”
PG์‚ฌ ์›นํ›…์€ ๋น ๋ฅด๊ฒŒ 200 ์‘๋‹ต ํ›„ ์‹ค์ œ ์ฒ˜๋ฆฌ๋Š” ๋น„๋™๊ธฐ๋กœ.
์›นํ›… ์ฒ˜๋ฆฌ ์‹คํŒจ ์‹œ ์žฌ์‹œ๋„ ๋กœ์ง ํ•„์ˆ˜.
4
์ธ๋ฑ์Šค ์ตœ์ ํ™”
guest_token, guest_email, order_token ์ปฌ๋Ÿผ์— ์ธ๋ฑ์Šค ์ถ”๊ฐ€.
์ฃผ๋ฌธ ์กฐํšŒ ์ฟผ๋ฆฌ ์„ฑ๋Šฅ์ด ํฌ๊ฒŒ ํ–ฅ์ƒ๋ผ.
-- ์ธ๋ฑ์Šค ์ถ”๊ฐ€
ALTER TABLE orders ADD INDEX idx_guest_token (guest_token);
ALTER TABLE orders ADD INDEX idx_guest_email (guest_email);
ALTER TABLE orders ADD INDEX idx_order_token (order_token);
ALTER TABLE cart_items ADD INDEX idx_cart_guest_token (guest_token);

โœ… ๊ฒŒ์ŠคํŠธ ์ฒดํฌ์•„์›ƒ ๊ตฌํ˜„ ์™„์„ฑ ์ฒดํฌ๋ฆฌ์ŠคํŠธ ๐Ÿ—๏ธ ๋ฐฑ์—”๋“œ ์„ค๊ณ„ orders ํ…Œ์ด๋ธ” user_id NULL ํ—ˆ์šฉ guest_token (UUID) ์ปฌ๋Ÿผ ์ถ”๊ฐ€ guest_email, guest_name ์ปฌ๋Ÿผ order_token (์กฐํšŒ์šฉ) ์ปฌ๋Ÿผ ํŠธ๋žœ์žญ์…˜ ์ฒ˜๋ฆฌ (์ฃผ๋ฌธ+์žฌ๊ณ +๋ฐฐ์†ก) DB ์ธ๋ฑ์Šค ์ตœ์ ํ™” ๐Ÿ” ๋ณด์•ˆ httpOnly ์ฟ ํ‚ค๋กœ ํ† ํฐ ์ €์žฅ Rate Limiting ์ ์šฉ CSRF ํ† ํฐ ๋ณดํ˜ธ ์ด๋ฉ”์ผ+ํ† ํฐ ์ด์ค‘ ๊ฒ€์ฆ HTTPS ๊ฐ•์ œ ์ ์šฉ ๐ŸŽจ ํ”„๋ก ํŠธ์—”๋“œ UX ์ฒดํฌ์•„์›ƒ ๋ฐฉ์‹ ์„ ํƒ ํ™”๋ฉด ๊ฒŒ์ŠคํŠธ ์ •๋ณด ์ž…๋ ฅ ํผ ์‹ค์‹œ๊ฐ„ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ ์ฃผ์†Œ ์ž๋™์™„์„ฑ API ์—ฐ๋™ ๋ชจ๋ฐ”์ผ ์ตœ์ ํ™” inputmode ์ฃผ๋ฌธ ์™„๋ฃŒ ํŽ˜์ด์ง€ ๊ฐ€์ž… ์œ ๋„ ๐Ÿ“ง ์ด๋ฉ”์ผ & ์ „ํ™˜ ์ฃผ๋ฌธ ํ™•์ธ ์ด๋ฉ”์ผ ๋ฐœ์†ก ์ฃผ๋ฌธ ์กฐํšŒ ๋งํฌ ํฌํ•จ ์žฅ๋ฐ”๊ตฌ๋‹ˆ ๋ณ‘ํ•ฉ ๋กœ์ง ๋น„ํšŒ์›โ†’ํšŒ์› ์ฃผ๋ฌธ ์—ฐ๊ฒฐ ์ด๋ฉ”์ผ ๋น„๋™๊ธฐ ๋ฐœ์†ก ์ฒ˜๋ฆฌ ๐ŸŽ‰ ๋ชจ๋‘ ์™„๋ฃŒํ•˜๋ฉด ์™„๋ฒฝํ•œ ๊ฒŒ์ŠคํŠธ ์ฒดํฌ์•„์›ƒ ์‹œ์Šคํ…œ ์™„์„ฑ!

๐Ÿ’Ž ์‹ค์ „ ํŒ โ€” ํ˜„์—…์—์„œ ์ž์ฃผ ๋งˆ์ฃผ์น˜๋Š” ์ƒํ™ฉ๋“ค

๐Ÿค” Q. ๋น„ํšŒ์› ์žฅ๋ฐ”๊ตฌ๋‹ˆ๋ฅผ DB์— ์ €์žฅํ•ด์•ผ ํ• ๊นŒ, ์ฟ ํ‚ค/localStorage์— ์ €์žฅํ•ด์•ผ ํ• ๊นŒ?

๐Ÿช ํด๋ผ์ด์–ธํŠธ ์ €์žฅ
(์ฟ ํ‚ค/localStorage)

โœ… ์„œ๋ฒ„ ๋ถ€ํ•˜ ์—†์Œ
โœ… ๊ตฌํ˜„ ๊ฐ„๋‹จ
โŒ ๋ธŒ๋ผ์šฐ์ € ์‚ญ์ œ ์‹œ ์†Œ์‹ค
โŒ ๋‹ค๋ฅธ ๊ธฐ๊ธฐ์—์„œ ์ ‘๊ทผ ๋ถˆ๊ฐ€
โŒ ์žฌ๊ณ  ์‹ค์‹œ๊ฐ„ ๋ฐ˜์˜ ์–ด๋ ค์›€
๐Ÿ—„๏ธ ์„œ๋ฒ„ DB ์ €์žฅ
(guest_token ๊ธฐ๋ฐ˜)

โœ… ๊ธฐ๊ธฐ ๊ฐ„ ๊ณต์œ  ๊ฐ€๋Šฅ
โœ… ์žฌ๊ณ  ์‹ค์‹œ๊ฐ„ ์ฒดํฌ
โœ… ๋ฐ์ดํ„ฐ ์•ˆ์ „
โŒ ์„œ๋ฒ„ ๋ถ€ํ•˜ ์ฆ๊ฐ€
โŒ ์ฃผ๊ธฐ์  ์ •๋ฆฌ ํ•„์š”
๐Ÿ’ก ๊ฒฐ๋ก : ์†Œ๊ทœ๋ชจ ์‡ผํ•‘๋ชฐ์€ localStorage, ์ค‘๋Œ€ํ˜•์€ DB ์ €์žฅ ๊ถŒ์žฅ.
DB ์ €์žฅ ์‹œ 30์ผ ์ด์ƒ ๋œ ๋น„ํšŒ์› ์žฅ๋ฐ”๊ตฌ๋‹ˆ๋Š” ์ฃผ๊ธฐ์ ์œผ๋กœ ์‚ญ์ œํ•˜๋Š” ๋ฐฐ์น˜ ์ž‘์—… ํ•„์š”.

๐Ÿค” Q. PG์‚ฌ ๊ฒฐ์ œ ์—ฐ๋™ ์‹œ ๋น„ํšŒ์› ์ฒ˜๋ฆฌ๋Š”?

๋Œ€๋ถ€๋ถ„์˜ PG์‚ฌ(ํ† ์ŠคํŽ˜์ด๋จผ์ธ , ์•„์ž„ํฌํŠธ/ํฌํŠธ์›, KG์ด๋‹ˆ์‹œ์Šค ๋“ฑ)๋Š”
๊ฒฐ์ œ ์š”์ฒญ ์‹œ ์ฃผ๋ฌธ์ž ์ •๋ณด๋ฅผ ์ง์ ‘ ์ „๋‹ฌํ•˜๋Š” ๋ฐฉ์‹์ด๋ผ ํšŒ์›/๋น„ํšŒ์› ๊ตฌ๋ถ„์ด ์—†์–ด.
์ค‘์š”ํ•œ ๊ฑด ๊ฒฐ์ œ ์™„๋ฃŒ ํ›„ ์›นํ›…(Webhook)์œผ๋กœ ์ฃผ๋ฌธ ์ƒํƒœ๋ฅผ ์—…๋ฐ์ดํŠธํ•˜๋Š” ๊ฑฐ์•ผ.

// ํ† ์ŠคํŽ˜์ด๋จผ์ธ  ๊ฒฐ์ œ ์™„๋ฃŒ ์›นํ›… ์ฒ˜๋ฆฌ
router.post('/webhook/toss', async (req, res) => {
  const { paymentKey, orderId, status, amount } = req.body;
  
  // ์›นํ›… ์„œ๋ช… ๊ฒ€์ฆ (๋ณด์•ˆ ํ•„์ˆ˜!)
  const isValid = verifyTossWebhook(req.headers, req.body);
  if (!isValid) return res.status(401).end();
  
  if (status === 'DONE') {
    // ๊ฒฐ์ œ ์™„๋ฃŒ ์ฒ˜๋ฆฌ
    await db.query(
      `UPDATE orders SET status = 'paid', payment_key = ? 
       WHERE order_number = ?`,
      [paymentKey, orderId]
    );
    
    // ์žฌ๊ณ  ์ฐจ๊ฐ (๊ฒฐ์ œ ์™„๋ฃŒ ํ›„ ์ฒ˜๋ฆฌ)
    await decreaseStock(orderId);
    
    // ๋ฐฐ์†ก ์ค€๋น„ ์•Œ๋ฆผ (๋น„ํšŒ์›์ด๋ฉด ์ด๋ฉ”์ผ๋กœ)
    await notifyOrderPaid(orderId);
  }
  
  // ์›นํ›…์€ ๋น ๋ฅด๊ฒŒ 200 ์‘๋‹ต
  res.status(200).end();
});

๐Ÿค” Q. ๋น„ํšŒ์› ์ฃผ๋ฌธ ๋ฐ์ดํ„ฐ ๋ณด์กด ๊ธฐ๊ฐ„์€?

๐Ÿ“‹ ๋ฒ•์  ๊ธฐ์ค€ (ํ•œ๊ตญ ์ „์ž์ƒ๊ฑฐ๋ž˜๋ฒ•):
โ€ข ๊ณ„์•ฝยท์ฒญ์•ฝ ์ฒ ํšŒ ๊ธฐ๋ก: 5๋…„
โ€ข ๋Œ€๊ธˆ ๊ฒฐ์ œ ๊ธฐ๋ก: 5๋…„
โ€ข ์†Œ๋น„์ž ๋ถˆ๋งŒยท๋ถ„์Ÿ ์ฒ˜๋ฆฌ ๊ธฐ๋ก: 3๋…„

๋น„ํšŒ์› ์ฃผ๋ฌธ๋„ ๋™์ผํ•˜๊ฒŒ ์ ์šฉ๋ผ. ์ž„์˜๋กœ ์‚ญ์ œํ•˜๋ฉด ์•ˆ ๋ผ!
๋‹จ, ์žฅ๋ฐ”๊ตฌ๋‹ˆ ๋ฐ์ดํ„ฐ(๋ฏธ์™„๋ฃŒ ์ฃผ๋ฌธ)๋Š” 30~90์ผ ํ›„ ์‚ญ์ œ ๊ฐ€๋Šฅ.

๐Ÿค” Q. ์žฌ๋Šฅ๋„ท ๊ฐ™์€ ์„œ๋น„์Šค ํ”Œ๋žซํผ์—์„œ์˜ ๋น„ํšŒ์› ์ฒ˜๋ฆฌ

์žฌ๋Šฅ๋„ท์ฒ˜๋Ÿผ ์žฌ๋Šฅ์„ ๊ฑฐ๋ž˜ํ•˜๋Š” ํ”Œ๋žซํผ์—์„œ๋„ ๋น„์Šทํ•œ ๊ฐœ๋…์ด ์ ์šฉ๋ผ.
์„œ๋น„์Šค๋ฅผ ์ฒ˜์Œ ํƒ์ƒ‰ํ•˜๋Š” ์‚ฌ๋žŒ์ด ํšŒ์›๊ฐ€์ž… ์—†์ด ๊ฒฌ์  ๋ฌธ์˜๋‚˜ ๊ฐ„๋‹จํ•œ ์ƒ๋‹ด์„ ํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•˜๋ฉด
์ง„์ž… ์žฅ๋ฒฝ์ด ๋‚ฎ์•„์ง€๊ณ  ์ „ํ™˜์œจ์ด ๋†’์•„์ง€๋Š” ํšจ๊ณผ๊ฐ€ ์žˆ์–ด.
ํ•ต์‹ฌ์€ "์ผ๋‹จ ๊ฒฝํ—˜ํ•˜๊ฒŒ ํ•˜๊ณ , ๊ทธ ๋‹ค์Œ์— ๊ฐ€์ž…์„ ์œ ๋„"ํ•˜๋Š” ํ๋ฆ„์ด์•ผ.


๐ŸŽฏ ๋งˆ๋ฌด๋ฆฌ โ€” ๊ฒŒ์ŠคํŠธ ์ฒดํฌ์•„์›ƒ ๊ตฌํ˜„ ํ•ต์‹ฌ ์š”์•ฝ

์ž, ์—ฌ๊ธฐ๊นŒ์ง€ ์™”์œผ๋ฉด ์ด์ œ ๊ฒŒ์ŠคํŠธ ์ฒดํฌ์•„์›ƒ์— ๋Œ€ํ•ด ๊ฝค ์ž˜ ์•Œ๊ฒŒ ๋์„ ๊ฑฐ์•ผ! ๐ŸŽ‰
ํ•ต์‹ฌ๋งŒ ๋‹ค์‹œ ํ•œ๋ฒˆ ์ •๋ฆฌํ•ด๋ณผ๊ฒŒ.

โœ…
UUID ๊ธฐ๋ฐ˜ guest_token์œผ๋กœ ๋น„ํšŒ์›์„ ์•ˆ์ „ํ•˜๊ฒŒ ์‹๋ณ„
โœ…
orders ํ…Œ์ด๋ธ” user_id NULL ํ—ˆ์šฉ์œผ๋กœ ํšŒ์›/๋น„ํšŒ์› ํ†ตํ•ฉ ๊ด€๋ฆฌ
โœ…
ํŠธ๋žœ์žญ์…˜์œผ๋กœ ์ฃผ๋ฌธ ์ƒ์„ฑ์˜ ์›์ž์„ฑ ๋ณด์žฅ
โœ…
์ด๋ฉ”์ผ + ํ† ํฐ ์ด์ค‘ ๊ฒ€์ฆ์œผ๋กœ ์ฃผ๋ฌธ ์กฐํšŒ ๋ณด์•ˆ ๊ฐ•ํ™”
โœ…
Rate Limiting + CSRF๋กœ ์•…์šฉ ๋ฐฉ์ง€
โœ…
์žฅ๋ฐ”๊ตฌ๋‹ˆ ๋ณ‘ํ•ฉ ๋กœ์ง์œผ๋กœ ๋กœ๊ทธ์ธ ์‹œ ๋ฐ์ดํ„ฐ ์œ ์ง€
โœ…
์ฃผ๋ฌธ ํ›„ ํšŒ์› ์ „ํ™˜ ์œ ๋„๋กœ ์ถฉ์„ฑ ๊ณ ๊ฐ ํ™•๋ณด
๐Ÿš€ ์ตœ์ข… ๋ฉ”์‹œ์ง€:
๊ฒŒ์ŠคํŠธ ์ฒดํฌ์•„์›ƒ์€ ๋‹จ์ˆœํžˆ "ํšŒ์›๊ฐ€์ž… ์—†์ด ์ฃผ๋ฌธ"์ด ์•„๋‹ˆ์•ผ.
๊ณ ๊ฐ์˜ ์ฒซ ๊ฒฝํ—˜์„ ์ตœ๋Œ€ํ•œ ๋ถ€๋“œ๋Ÿฝ๊ฒŒ ๋งŒ๋“ค๊ณ , ๊ทธ ๊ฒฝํ—˜์„ ๋ฐ”ํƒ•์œผ๋กœ ์žฅ๊ธฐ ๊ณ ๊ฐ์œผ๋กœ ์ „ํ™˜์‹œํ‚ค๋Š” ์ „๋žต์ด์•ผ.
์ž˜ ๊ตฌํ˜„๋œ ๊ฒŒ์ŠคํŠธ ์ฒดํฌ์•„์›ƒ ํ•˜๋‚˜๊ฐ€ ์‡ผํ•‘๋ชฐ ๋งค์ถœ์„ 30% ์ด์ƒ ์˜ฌ๋ฆด ์ˆ˜ ์žˆ์–ด.
์ด์ œ ์ง์ ‘ ๊ตฌํ˜„ํ•ด๋ด! ๋„Œ ํ•  ์ˆ˜ ์žˆ์–ด ๐Ÿ’ช
๋Œ“๊ธ€ ์ž‘์„ฑ

์ด ๊ธ€์— ๋Œ€ํ•œ ์—ฌ๋Ÿฌ๋ถ„์˜ ์ƒ๊ฐ์„ ๋“ค๋ ค์ฃผ์„ธ์š”

๋Œ“๊ธ€ 0
๋Œ“๊ธ€์„ ๋ถˆ๋Ÿฌ์˜ค๋Š” ์ค‘์ž…๋‹ˆ๋‹ค.

์•„์ง ๋Œ“๊ธ€์ด ์—†์Šต๋‹ˆ๋‹ค.