Fullstack-Study-241204-250625

๐ŸŽฅ ์‹ค์‹œ๊ฐ„ ๋ฐฉ์†ก(WebRTC)์„ ํ™œ์šฉํ•œ ์˜จ๋ผ์ธ ๊ฒฝ๋งค ํ”Œ๋žซํผ

BIDCAST๋Š” ๋ˆ„๊ตฌ๋‚˜ ๊ฒฝ๋งค๋ฅผ ๊ฐœ์„คํ•˜๊ณ  ์ž…์ฐฐ์— ์ฐธ์—ฌํ•  ์ˆ˜ ์žˆ๋Š” ์‹ค์‹œ๊ฐ„ ์˜์ƒ ๊ธฐ๋ฐ˜ ๊ฒฝ๋งค ์‹œ์Šคํ…œ์ž…๋‹ˆ๋‹ค.
์‹ค์‹œ๊ฐ„ ์ฑ„ํŒ…, ์˜์ƒ ๊ณต์œ , ์ž…์ฐฐ ๊ธฐ๋Šฅ์„ ํ†ตํ•ด ์ƒ๋™๊ฐ ์žˆ๋Š” ๊ฒฝ๋งค ๊ฒฝํ—˜์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ”— BIDCAST ์„œ๋ฒ„ GitHub ๐Ÿ”— BIDCAST ํด๋ผ์ด์–ธํŠธ GitHub ๐Ÿ”— AWS์„ธํŒ…ํ•˜๊ธฐ ๐ŸŒ BIDCAST ํ™ˆํŽ˜์ด์ง€


๐Ÿ”ง ์ฃผ์š” ๊ธฐ๋Šฅ


๐Ÿ“ฆ ํ”„๋กœ์ ํŠธ ๊ตฌ์„ฑ ๋ฐ ๋ฐฐํฌ

โš ๏ธ ์ฃผ์˜: application-custom.properties ํŒŒ์ผ์€ Git์— ํฌํ•จ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ์ง์ ‘ src/main/resources ๊ฒฝ๋กœ์— ์•„๋ž˜ ๋‚ด์šฉ์„ ํฌํ•จํ•œ ํŒŒ์ผ์„ ์ถ”๊ฐ€ํ•ด์ฃผ์„ธ์š”:

application-custom.properties ์˜ˆ์‹œ ๋ณด๊ธฐ
properties
spring.datasource.url=jdbc:postgresql://<DB์ฃผ์†Œ>:5432/bidcast
spring.datasource.username=<DB์œ ์ €>
spring.datasource.password=<DB๋น„๋ฐ€๋ฒˆํ˜ธ>

jwt.secret=<JWT ๋น„๋ฐ€ ํ‚ค>
jwt.expiration=3600000

aws.s3.access-key=<AccessKey>
aws.s3.secret-key=<SecretKey>
aws.s3.region=ap-northeast-2
aws.s3.bucket=<๋ฒ„ํ‚ท๋ช…>
aws.s3.folder=uploads

๐Ÿ›  ์‚ฌ์šฉ ๊ธฐ์ˆ  ์Šคํƒ

1. Server

1-1. App Server (์œ ์ € ์„œ๋น„์Šค)

1-2. SFU Server (์ค‘๊ณ„ ์„œ๋ฒ„)

1-3. Database

1-4. Cloud Storage

2. Infrastructure

3. Dev Tools

4. Collaboration

5. CI/CD


์„œ๋ฒ„ ์ฝ”๋“œ๋ถ„์„

1.WebSocket & WebRTC

๋‚ด์šฉ ๋ณด๊ธฐ

์ข…๋ฅ˜

๋ฐฉ์‹


WebRTC & mediasoup

WebRTC

ICE, STUN, TURN

์šฉ์–ด ์„ค๋ช…
ICE ์—ฐ๊ฒฐ ๊ฐ€๋Šฅํ•œ ํ›„๋ณด ์ฃผ์†Œ๋“ค์„ ์ˆ˜์ง‘ํ•ด ์ตœ์  ๊ฒฝ๋กœ๋ฅผ ์„ ํƒํ•˜๋Š” ํ”„๋ ˆ์ž„์›Œํฌ
STUN ๊ณต์ธ IP ๋ฐ ํฌํŠธ ์ •๋ณด๋ฅผ ์•Œ์•„๋‚ด๊ธฐ ์œ„ํ•œ ์„œ๋ฒ„
TURN P2P ์—ฐ๊ฒฐ์ด ๋ถˆ๊ฐ€ํ•œ ๊ฒฝ์šฐ ๋ฏธ๋””์–ด๋ฅผ ์ค‘๊ณ„ํ•ด์ฃผ๋Š” ์„œ๋ฒ„ (๋Œ€์—ญํญ ์†Œ๋ชจ โ†‘)

mediasoup

mediasoup ์ฃผ์š” ๊ฐœ๋…

์šฉ์–ด ์„ค๋ช…
Worker ๋ฏธ๋””์–ด ์ฒ˜๋ฆฌ๋ฅผ ๋‹ด๋‹นํ•˜๋Š” ๋ฐฑ๊ทธ๋ผ์šด๋“œ ํ”„๋กœ์„ธ์Šค
Router ํ•œ ๋ฐฉ(room)์— ํ•˜๋‚˜์”ฉ ์กด์žฌ, ๋ฏธ๋””์–ด ๊ฒฝ๋กœ ์ œ์–ด ์—ญํ• 
Transport ํด๋ผ์ด์–ธํŠธ์™€ ์„œ๋ฒ„ ๊ฐ„ ๋ฏธ๋””์–ด ์—ฐ๊ฒฐ ํ†ต๋กœ (DTLS/ICE ๋“ฑ ํฌํ•จ)
Producer ํด๋ผ์ด์–ธํŠธ๊ฐ€ ๋ฏธ๋””์–ด๋ฅผ ์†ก์ถœํ•  ๋•Œ ์ƒ์„ฑ๋˜๋Š” ๊ฐ์ฒด
Consumer ํด๋ผ์ด์–ธํŠธ๊ฐ€ ๋ฏธ๋””์–ด๋ฅผ ์ˆ˜์‹ ํ•  ๋•Œ ์ƒ์„ฑ๋˜๋Š” ๊ฐ์ฒด
์ƒ์„ธ ์„ค๋ช…
  • Worker : ๋ฏธ๋””์–ด ์ฒ˜๋ฆฌ๋ฅผ ๋‹ด๋‹นํ•˜๋Š” ์—”์ง„ ๊ฐ™์€ ํ”„๋กœ์„ธ์Šค, ์‹ค์ œ ๋ฏธ๋””์–ด ๋ฐ์ดํ„ฐ(์Œ์„ฑ, ์˜์ƒ)๋ฅผ ๋‹ค๋ฃธ
  • Router : producer๊ฐ€ ๋ณด๋‚ด๋Š” ๋ฏธ๋””์–ด ์ŠคํŠธ๋ฆผ์„ ๋ฐ›์•„์„œ ์–ด๋–ค consumer์—๊ฒŒ ๋ณด๋‚ผ์ง€ ์ •ํ•จ. ํ•˜๋‚˜์˜ ๋ฐฉ์—๋Š” ํ•˜๋‚˜์˜ Router๊ฐ€ ์žˆ์Œ
  • Transport : ICE ์—ฐ๊ฒฐ, DTLS ํ•ธ๋“œ์…ฐ์ดํฌ ๋“ฑ์„ ํฌํ•จํ•œ ํ†ต์‹  ๊ฒฝ๋กœ, consumer/producer ๊ฐ๊ฐ ์“ฐ๋Š” tranport๊ฐ€ ๋‹ค๋ฆ„

rtpCapabilities

DTLS


์„œ๋ฒ„ ๋ฏธ๋””์–ด ์ฒ˜๋ฆฌ ํ๋ฆ„

์ ‘๊ธฐ/ํŽผ์น˜๊ธฐ

1) rtpCapabilities ์š”์ฒญ

socket.on('create-router', (_, callback) => {
  callback({ rtpCapabilities: router.rtpCapabilities });
});

2) Transport ์ƒ์„ฑ

create-transport ์ฝ”๋“œ ๋ณด๊ธฐ
socket.on('create-transport', async ({ direction }, callback) => {
  const transport = await router.createWebRtcTransport({
    listenIps: [{ ip: '0.0.0.0', announcedIp: 'bidcastserver.kro.kr' }],
    enableUdp: true,
    enableTcp: true,
    preferUdp: true,
    portRange: { min: 40000, max: 40010 },
    iceServers: [
      { urls: 'stun:stun.l.google.com:19302' },
      { urls: 'turn:bidcastserver.kro.kr:3478', username: 'webrtc', credential: '1234' }
    ],
  });
  transports.set(transport.id, { transport, socketId: socket.id, direction });

  callback({
    id: transport.id,
    iceParameters: transport.iceParameters,
    iceCandidates: transport.iceCandidates,
    dtlsParameters: transport.dtlsParameters,
  });
});

3) Transport ์—ฐ๊ฒฐ (DTLS ํ•ธ๋“œ์‰์ดํฌ)

socket.on('connect-transport', async ({ dtlsParameters, transportId }, callback) => {
  const data = transports.get(transportId);
  if (!data) throw new Error('Transport not found');
  await data.transport.connect({ dtlsParameters });
  callback();
});

4) Producer ์ƒ์„ฑ (๋ฏธ๋””์–ด ์†ก์ถœ ์‹œ์ž‘)

produce ์š”์ฒญ ์ฒ˜๋ฆฌ
socket.on('produce', async ({ kind, rtpParameters, transportId, roomId }, callback) => {
  const data = transports.get(transportId);
  if (!data) throw new Error('Transport not found');
  const producer = await data.transport.produce({ kind, rtpParameters });

  if (!producers.has(roomId)) producers.set(roomId, new Map());
  producers.get(roomId).set(producer.id, { producer, socketId: socket.id, kind });

  socket.broadcast.to(roomId).emit('new-producer', {
    producerId: producer.id,
    socketId: socket.id,
    kind,
  });

  callback({ id: producer.id });
});

5) Consumer ์ƒ์„ฑ (๋ฏธ๋””์–ด ์ˆ˜์‹  ์š”์ฒญ)

consume ์š”์ฒญ ์ฒ˜๋ฆฌ
socket.on('consume', async ({ producerId, rtpCapabilities, transportId, roomId }, callback) => {
  const data = transports.get(transportId);
  if (!data) throw new Error('Transport not found');

  const roomProducers = producers.get(roomId);
  if (!roomProducers || !roomProducers.has(producerId)) throw new Error('Producer not found');

  if (!router.canConsume({ producerId, rtpCapabilities })) throw new Error('Cannot consume');

  const consumer = await data.transport.consume({
    producerId,
    rtpCapabilities,
    paused: false,
  });

  consumers.set(consumer.id, { consumer, socketId: socket.id });

  callback({
    id: consumer.id,
    producerId,
    kind: consumer.kind,
    rtpParameters: consumer.rtpParameters,
  });
});

6) Consumer ์žฌ์ƒ ์‹œ์ž‘

socket.on('consumer-resume', async ({ consumerId }) => {
  const data = consumers.get(consumerId);
  if (!data) return;
  await data.consumer.resume();
});

7) ์—ฐ๊ฒฐ ์ข…๋ฃŒ ์‹œ ์ž์› ์ •๋ฆฌ

disconnect ์ฒ˜๋ฆฌ ์ „์ฒด ์ฝ”๋“œ
socket.on('disconnect', () => {
  for (const [transportId, data] of transports) {
    if (data.socketId === socket.id) {
      data.transport.close();
      transports.delete(transportId);
    }
  }
  for (const [roomId, roomProducers] of producers) {
    for (const [producerId, data] of roomProducers) {
      if (data.socketId === socket.id) {
        data.producer.close();
        roomProducers.delete(producerId);
        io.emit('user-disconnected', {
          socketId: socket.id,
          producerId,
        });
      }
    }
    if (roomProducers.size === 0) producers.delete(roomId);
  }
  for (const [consumerId, data] of consumers) {
    if (data.socketId === socket.id) {
      data.consumer.close();
      consumers.delete(consumerId);
    }
  }
});

2. ์„œ๋ฒ„ ์ฃผ์š” ์ „์—ญ ๋ณ€์ˆ˜ ๋ฐ ํ•จ์ˆ˜ ์ •๋ฆฌ

๋‚ด์šฉ ๋ณด๊ธฐ

1. mediasoup ๊ด€๋ จ ๋ณ€์ˆ˜

2. ์†Œ์ผ“ & ๋ฐฉ ๊ด€๋ฆฌ ๋ณ€์ˆ˜

3. ์ƒํ’ˆ ๋ฐ์ดํ„ฐ ์ •๊ทœํ™” ํ•จ์ˆ˜

function normalizeProduct(raw) {
  return {
    prodKey: raw.prod_key,
    aucKey: raw.auc_key,
    prodName: raw.prod_name,
    prodDetail: raw.prod_detail,
    unitValue: raw.unit_value,
    initPrice: raw.init_price,
    currentPrice: raw.current_price,
    finalPrice: raw.final_price,
    winnerId: raw.winner_id,
    prodStatus: raw.prod_status,
    fileUrl: raw.file_url
  };
}

3. ํด๋ผ์ด์–ธํŠธ-์„œ๋ฒ„ ํ†ต์‹  ํ๋ฆ„

๋‚ด์šฉ ๋ณด๊ธฐ

1. ๊ฒฝ๋งค์žฅ ์ž…์žฅ/ํ‡ด์žฅ

์ž…์žฅ

ํ‡ด์žฅ

2. ์˜์ƒ ์†ก์ถœ (mediasoup)

๋ฏธ๋””์–ด ์ŠคํŠธ๋ฆผ ์ˆ˜์‹  ๋ฐœ์†ก

๊ธฐ์กด Producer ๋ฆฌ์ŠคํŠธ ์š”์ฒญ

Producer ์‚ญ์ œ

๋ฏธ๋””์–ด ์ŠคํŠธ๋ฆผ ์ˆ˜์‹  ์š”์ฒญ

์žฌ์ƒ์‹œ์ž‘

3. ์ฑ„ํŒ…

์ฑ„ํŒ…

4. ์ž…์ฐฐ

์ž…์ฐฐ์‹œ๋„

5. ๊ฒฝ๋งค ๊ด€๋ฆฌ (ํ˜ธ์ŠคํŠธ)

์ƒํ’ˆ ์„ ํƒ

๋‚™์ฐฐ / ์œ ์ฐฐ ์ƒํƒœ ๋ณ€๊ฒฝ

์ตœ๊ณ  ์ž…์ฐฐ์ž ๋˜๋Œ๋ฆฌ๊ธฐ

์ž…์ฐฐ ๋‹จ์œ„ ๋ณ€๊ฒฝ

๊ฒฝ๋งค ์ข…๋ฃŒ

6. ๊ธฐํƒ€

์‹œ์ฒญ์ž ์ˆ˜ ์กฐํšŒ


ํด๋ผ์ด์–ธํŠธ ์ฝ”๋“œ๋ถ„์„

1. ๋นŒ๋“œ ๋ฐ ๋ฐฐํฌ ๊ด€๋ จ ์„ค์ •

๋‚ด์šฉ ๋ณด๊ธฐ
vite.config.js ์ฃผ์š” ์„ค์ •
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import fs from 'fs';
import path from 'path';

export default defineConfig({
  plugins: [react()],
  root: 'src/main/react',
  build: {
    outDir: '../resources/static/bundle',
    emptyOutDir: true,
    cssCodeSplit: true,
    rollupOptions: {
      input: {
        home: path.resolve(__dirname, 'src/home/home.jsx'),
        login: path.resolve(__dirname, 'src/auth/login/login.jsx'),
        bidGuest: path.resolve(__dirname, 'src/bidGuest/bidGuest.jsx'),
        // ... ์ƒ๋žต ๊ฐ€๋Šฅ
      },
      output: {
        entryFileNames: 'js/[name].bundle.js',
        chunkFileNames: 'chunk/[name].chunk.js',
        assetFileNames: `css/[name].css`,
      },
    },
  },
  server: {
    https: {
      key: fs.readFileSync('certs/key.pem'),
      cert: fs.readFileSync('certs/cert.pem'),
    },
    host: '0.0.0.0',
    port: 3200,
  },
});
Thymeleaf ํ…œํ”Œ๋ฆฟ ์˜ˆ์‹œ
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
  <title>[[${pageName}]]</title>
  <link rel="stylesheet" th:href="@{'/bundle/css/' + ${pageName} + '.css'}" onerror="this.remove()" />
</head>
<body>
  <div id="root"></div>
  <script type="module" th:src="@{'/bundle/js/' + ${pageName} + '.bundle.js'}"></script>
</body>
</html>
Spring Boot Controller โ€” ๋ฉ€ํ‹ฐ ํŽ˜์ด์ง€ ๋งคํ•‘
package com.project.bidcast.controller;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;

import javax.servlet.http.HttpSession;

@Controller
public class MainController {

    @GetMapping("/")
    public String redirectToHome() {
        return "redirect:/home.do";
    }

    @GetMapping("/{pageName}.do") // ๋ชจ๋“  ํŽ˜์ด์ง€ ์š”์ฒญ์„ ์ฒ˜๋ฆฌ
    public String page(HttpSession session , @PathVariable String pageName, Model model) {
        model.addAttribute("pageName", pageName); // Thymeleaf์— pageName ์ „๋‹ฌ
        System.out.println("๋ทฐ์ด๋ฆ„:" + pageName);

        if(session.getAttribute("id") != null) 
            System.out.println(session.getAttribute("id"));

        return "view"; // ํ•ญ์ƒ ๋™์ผํ•œ view.html ํ…œํ”Œ๋ฆฟ์œผ๋กœ ์ด๋™
    }
}
vite.config.js์˜ ๋กœ์ปฌ ๊ฐœ๋ฐœ์šฉ HTTPS ์„ค์ • (๋ฐฐํฌ ์‹œ ๋ฏธ์‚ฌ์šฉ)
server: {
  https: {
    key: fs.readFileSync('certs/key.pem'),
    cert: fs.readFileSync('certs/cert.pem'),
  },
  host: '0.0.0.0',
  port: 3200,
}

2. WebSocket & WebRTC

๋‚ด์šฉ ๋ณด๊ธฐ

1.1 Host (ํ˜ธ์ŠคํŠธ) ์ธก ๊ตฌํ˜„

1.1.1 ์ฃผ์š” ๋ณ€์ˆ˜ ๋ฐ ์ƒํƒœ

๋ณ€์ˆ˜๋ช… ์„ค๋ช…
peers ์ „์ฒด ์ฐธ๊ฐ€์ž์˜ ์†Œ์ผ“ ID๋ฅผ ํ‚ค๋กœ ํ•˜๋Š” ๊ฐ์ฒด
์˜ˆ: { socketId: { stream } }
hostSocketId ํ˜ธ์ŠคํŠธ(๋ฐฉ์†ก ์†ก์ถœ์ž)์˜ ์†Œ์ผ“ ID
mySocketId ํ˜„์žฌ ํด๋ผ์ด์–ธํŠธ(ํ˜ธ์ŠคํŠธ)์˜ ์†Œ์ผ“ ID
userInfoMap ์ฐธ๊ฐ€์ž๋ณ„ ๋‹‰๋„ค์ž„, ์ž…์ฐฐ๊ฐ€ ๋“ฑ์ด ์ €์žฅ๋œ ๊ฐ์ฒด
selectedProductIdx ํ˜„์žฌ ๊ฒฝ๋งค ์ค‘์ธ ์ƒํ’ˆ์˜ ์ธ๋ฑ์Šค
products ๊ฒฝ๋งค ์ƒํ’ˆ ๋ฆฌ์ŠคํŠธ ๋ฐฐ์—ด
prevHighestBidder ์ด์ „ ์ตœ๊ณ  ์ž…์ฐฐ์ž ์ •๋ณด (์žฌ์ž…์ฐฐ ๋ฐ˜์˜ ์‹œ ์‚ฌ์šฉ)

1.1.2 ์ฃผ์š” WebSocket ์ด๋ฒคํŠธ ๋ฐ ์š”์ฒญ

์ด๋ฒคํŠธ๋ช… ๋ชฉ์  ๋ฐ ์„ค๋ช… ๋ฐœ์‹ ์ž ์ˆ˜์‹ ์ž
host-selected-product ํ˜ธ์ŠคํŠธ๊ฐ€ ํŠน์ • ๊ฒฝ๋งค ์ƒํ’ˆ์„ ์„ ํƒํ–ˆ์Œ์„ ์„œ๋ฒ„์— ์•Œ๋ฆผ ํ˜ธ์ŠคํŠธ ์„œ๋ฒ„, ํด๋ผ์ด์–ธํŠธ
bid-status ๋‚™์ฐฐ(์™„๋ฃŒ), ์œ ์ฐฐ(์‹คํŒจ) ์ƒํƒœ๋ฅผ ์„œ๋ฒ„์— ์ „๋‹ฌ ํ˜ธ์ŠคํŠธ ์„œ๋ฒ„, ํด๋ผ์ด์–ธํŠธ
revert-bidder ์ตœ๊ณ  ์ž…์ฐฐ์ž๋ฅผ ์ด์ „ ์ž…์ฐฐ์ž๋กœ ๋˜๋Œ๋ฆด ๋•Œ ์„œ๋ฒ„์— ์š”์ฒญ ํ˜ธ์ŠคํŠธ ์„œ๋ฒ„, ํด๋ผ์ด์–ธํŠธ

1.1.3 WebRTC ๋ฏธ๋””์–ด ์ŠคํŠธ๋ฆผ ๊ด€๋ฆฌ


1.2 Client (๊ฒŒ์ŠคํŠธ) ์ธก ๊ตฌํ˜„

1.2.1 ์ฃผ์š” ๋ณ€์ˆ˜ ๋ฐ ์ƒํƒœ

๋ณ€์ˆ˜๋ช… ์„ค๋ช…
peers ์ „์ฒด ์ฐธ๊ฐ€์ž์˜ ์†Œ์ผ“ ID๋ฅผ ํ‚ค๋กœ ํ•˜๋Š” ๊ฐ์ฒด
์˜ˆ: { socketId: { stream } }
hostSocketId ํ˜ธ์ŠคํŠธ ์†Œ์ผ“ ID
mySocketId ํ˜„์žฌ ํด๋ผ์ด์–ธํŠธ(๊ฒŒ์ŠคํŠธ)์˜ ์†Œ์ผ“ ID
userInfoMap ์ฐธ๊ฐ€์ž๋ณ„ ๋‹‰๋„ค์ž„, ์ž…์ฐฐ๊ฐ€ ๋“ฑ์ด ์ €์žฅ๋œ ๊ฐ์ฒด
product ํ˜„์žฌ ๊ฒฝ๋งค ์ค‘์ธ ์ƒํ’ˆ ์ •๋ณด

1.2.2 ์ฃผ์š” WebSocket ์ด๋ฒคํŠธ ๋ฐ ์š”์ฒญ

์ด๋ฒคํŠธ๋ช… ๋ชฉ์  ๋ฐ ์„ค๋ช… ๋ฐœ์‹ ์ž ์ˆ˜์‹ ์ž
bid-attempt ์ž…์ฐฐ ์‹œ๋„ ์š”์ฒญ ๊ฒŒ์ŠคํŠธ ์„œ๋ฒ„, ํด๋ผ์ด์–ธํŠธ
bid-update ์ž…์ฐฐ๊ฐ€ ๋ณ€๊ฒฝ ์•Œ๋ฆผ ์„œ๋ฒ„ ๋ชจ๋“  ํด๋ผ์ด์–ธํŠธ
host-selected-product ํ˜ธ์ŠคํŠธ๊ฐ€ ์ƒํ’ˆ์„ ์„ ํƒํ–ˆ์Œ์„ ์•Œ๋ฆผ ์„œ๋ฒ„ ํด๋ผ์ด์–ธํŠธ ์ „์›
bid-status ๋‚™์ฐฐ, ์œ ์ฐฐ ์ƒํƒœ ์—…๋ฐ์ดํŠธ ์„œ๋ฒ„ ํด๋ผ์ด์–ธํŠธ ์ „์›

1.2.3 WebRTC ๋ฏธ๋””์–ด ์ŠคํŠธ๋ฆผ ๊ด€๋ฆฌ

3. ํด๋ผ์ด์–ธํŠธ - ์„œ๋ฒ„ ํ†ต์‹  ํ๋ฆ„

๊ธฐ๋Šฅ ์‹œ์ž‘ ์ฃผ์ฒด ์ด๋ฒคํŠธ ์ด๋ฆ„ ์„œ๋ฒ„ ์ฒ˜๋ฆฌ ๋‚ด์šฉ ํด๋ผ์ด์–ธํŠธ ์ฒ˜๋ฆฌ ๋‚ด์šฉ
์ž…์ฐฐ ์‹œ๋„ ๊ฒŒ์ŠคํŠธ bid-attempt ์ž…์ฐฐ๊ฐ€ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ ๋ฐ ์ตœ๊ณ  ์ž…์ฐฐ์ž ๊ฐฑ์‹  ํ›„ bid-update ๋ฐฉ์†ก bid-update ์ˆ˜์‹  ํ›„ UI ๋ฐ ์ƒํƒœ ๊ฐฑ์‹ 
๊ฒฝ๋งค ์ƒํ’ˆ ์„ ํƒ ํ˜ธ์ŠคํŠธ host-selected-product ์ƒํ’ˆ ๋ณ€๊ฒฝ ์ €์žฅ ๋ฐ ๋ชจ๋“  ํด๋ผ์ด์–ธํŠธ์— ๋ฐฉ์†ก ์ƒํ’ˆ ์ •๋ณด ๊ฐฑ์‹  ๋ฐ UI ๋ณ€๊ฒฝ
๋‚™์ฐฐ/์œ ์ฐฐ ์ƒํƒœ ๋ณ€๊ฒฝ ํ˜ธ์ŠคํŠธ bid-status ๊ฒฝ๋งค ์ƒํƒœ ์ €์žฅ ๋ฐ ๋ฐฉ์†ก ์ƒํƒœ ๋ฉ”์‹œ์ง€ ๋ฐ UI ๊ฐฑ์‹ 
์ตœ๊ณ  ์ž…์ฐฐ์ž ๋˜๋Œ๋ฆฌ๊ธฐ ํ˜ธ์ŠคํŠธ revert-bidder ์ตœ๊ณ  ์ž…์ฐฐ์ž ์ •๋ณด ์žฌ์„ค์ • ๋ฐ ๋ฐฉ์†ก ์ž…์ฐฐ์ž ์ •๋ณด ์žฌ๊ฐฑ์‹ 
WebRTC ๋ฏธ๋””์–ด ์†ก์ถœ ํ˜ธ์ŠคํŠธ/๊ฒŒ์ŠคํŠธ WebRTC ์‹œ๊ทธ๋„๋ง SFU ์—ญํ• ๋กœ ๋ฏธ๋””์–ด ์ŠคํŠธ๋ฆผ ์ค‘๊ณ„ ์ŠคํŠธ๋ฆผ ์ƒ์„ฑ ๋ฐ ์ˆ˜์‹ , ๋น„๋””์˜ค ์ปดํฌ๋„ŒํŠธ์— ์ถœ๋ ฅ
์‹ค์‹œ๊ฐ„ ์ฑ„ํŒ… ํ˜ธ์ŠคํŠธ/๊ฒŒ์ŠคํŠธ chat-message ๋ฉ”์‹œ์ง€ ์ค‘๊ณ„ ๋ฐ ๋ธŒ๋กœ๋“œ์บ์ŠคํŠธ ๋ฉ”์‹œ์ง€ ์ˆ˜์‹  ๋ฐ UI ์—…๋ฐ์ดํŠธ

4. ์ธ์ฆ ๋ฐ ๊ถŒํ•œ ๊ด€๋ฆฌ ํ๋ฆ„ (Spring Security ๊ด€๋ จ ํฌํ•จ)

๋‚ด์šฉ ๋ณด๊ธฐ

4.1 ์ฃผ์š” ๊ธฐ๋Šฅ ๊ฐœ์š”


4.2 ์„ค์ • ์ฃผ์š” ์ฝ”๋“œ ์„ค๋ช…

์„ค์ • ํ•ญ๋ชฉ ๋‚ด์šฉ ๋ฐ ์—ญํ• 
passwordEncoder() ๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ BCrypt ๋ฐฉ์‹์œผ๋กœ ์•”ํ˜ธํ™”
csrf().disable() CSRF ๊ณต๊ฒฉ ๋ฐฉ์–ด ๊ธฐ๋Šฅ ๋น„ํ™œ์„ฑํ™” (API ํ™˜๊ฒฝ์— ์ ํ•ฉ)
authorizeHttpRequests() ์ธ์ฆ ์—†์ด ์ ‘๊ทผ ๊ฐ€๋Šฅํ•œ URL ๊ฒฝ๋กœ ์ง€์ •
.anyRequest().authenticated() ๋‚˜๋จธ์ง€ ๋ชจ๋“  ์š”์ฒญ์€ ์ธ์ฆ ํ•„์š”
sessionManagement() ์„ธ์…˜ ์ƒ์„ฑ ์ •์ฑ… ์„ค์ • (IF_REQUIRED: ํ•„์š”์‹œ๋งŒ ์ƒ์„ฑ)
exceptionHandling() ์ธ์ฆ ์‹คํŒจ ์‹œ ๋กœ๊ทธ์ธ ํŽ˜์ด์ง€๋กœ ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ ์ฒ˜๋ฆฌ
formLogin() ๋กœ๊ทธ์ธ ํŽ˜์ด์ง€, ์ฒ˜๋ฆฌ URL, ์„ฑ๊ณต ๋ฐ ์‹คํŒจ ํ•ธ๋“ค๋Ÿฌ ์„ค์ •
logout() ๋กœ๊ทธ์•„์›ƒ URL, ์„ฑ๊ณต ํ›„ ์ด๋™ ํŽ˜์ด์ง€, ์„ธ์…˜ ๋ฌดํšจํ™”, ์ฟ ํ‚ค ์‚ญ์ œ