Skip to content

Kyle-TM99/OneStack

Folders and files

NameName
Last commit message
Last commit date

Latest commit

ย 

History

490 Commits
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 

Repository files navigation

๐Ÿ›  OneStack - IT ์ „๋ฌธ๊ฐ€ ๋งค์นญ ํ”Œ๋žซํผ

OneStack Logo

๐Ÿ“Œ ํ”„๋กœ์ ํŠธ ์†Œ๊ฐœ

OneStack์€ IT ์ „๋ฌธ๊ฐ€์™€ ์˜๋ขฐ์ธ์„ ์—ฐ๊ฒฐํ•˜๋Š” ๋งค์นญ ํ”Œ๋žซํผ์ž…๋‹ˆ๋‹ค.
๊ฒ€์ฆ๋œ ์ „๋ฌธ๊ฐ€ ํ’€์„ ํ†ตํ•ด ์‹ ๋ขฐ์„ฑ ์žˆ๋Š” ์™ธ์ฃผ ์„œ๋น„์Šค๋ฅผ ์ œ๊ณตํ•˜๋ฉฐ, ์‹ค์‹œ๊ฐ„ ์ฑ„ํŒ…๊ณผ ํ”„๋กœ์ ํŠธ ๊ด€๋ฆฌ ๊ธฐ๋Šฅ์œผ๋กœ ํšจ์œจ์ ์ธ ํ˜‘์—…์„ ์ง€์›ํ•ฉ๋‹ˆ๋‹ค.

  • ๊ฐœ๋ฐœ ๊ธฐ๊ฐ„: 2024.01 ~ 2024.03 (3๊ฐœ์›”)
  • ์ธ์›: 1๋ช… (๊ฐœ์ธ ํ”„๋กœ์ ํŠธ)
  • ๋ฐฐํฌ URL: ONE STACK

๐ŸŽฏ ํ•ต์‹ฌ ๊ฐ€์น˜

  • ์‹ ๋ขฐ์„ฑ: ๊ฒ€์ฆ๋œ ์ „๋ฌธ๊ฐ€ ๋งค์นญ ์‹œ์Šคํ…œ
  • ํšจ์œจ์„ฑ: ์‹ค์‹œ๊ฐ„ ์†Œํ†ต ๋ฐ ํ”„๋กœ์ ํŠธ ๊ด€๋ฆฌ
  • ํˆฌ๋ช…์„ฑ: ๋ช…ํ™•ํ•œ ๊ฐ€๊ฒฉ ์ •์ฑ…๊ณผ ๋ฆฌ๋ทฐ ์‹œ์Šคํ…œ

๐Ÿ›  ๊ธฐ์ˆ  ์Šคํƒ

Frontend

HTML5 CSS3 JavaScript jQuery Thymeleaf Bootstrap

Backend

Java Spring Boot MyBatis WebSocket

Database & Infrastructure

MySQL AWS EC2 NGINX Docker Jenkins

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

1. ์ „๋ฌธ๊ฐ€ ๋งค์นญ ์‹œ์Šคํ…œ

  • ์นดํ…Œ๊ณ ๋ฆฌ๋ณ„ ์ „๋ฌธ๊ฐ€ ํ•„ํ„ฐ๋ง: ๊ธฐ์ˆ  ๋ถ„์•ผ๋ณ„ ์ „๋ฌธ๊ฐ€ ๊ฒ€์ƒ‰ ๋ฐ ํ•„ํ„ฐ๋ง
  • ๋ฌดํ•œ ์Šคํฌ๋กค: ํšจ์œจ์ ์ธ ์ „๋ฌธ๊ฐ€ ๋ฆฌ์ŠคํŠธ ๋กœ๋”ฉ
  • ์ „๋ฌธ๊ฐ€ ํ”„๋กœํ•„: ์ž์„ธํ•œ ํ”„๋กœํ•„ ๋ฐ ํฌํŠธํด๋ฆฌ์˜ค ๊ด€๋ฆฌ
@PostMapping("/proFilter")
public Map<String, Object> filterPros2(@RequestBody Map<String, Object> requestData) {
    List<String> appType = (List<String>) requestData.get("filters");
    String sort = (String) requestData.get("sort");
    int itemNo = (int) requestData.get("itemNo");
    int page = (int) requestData.get("page");
    int size = (int) requestData.get("size");

    // ํ•„ํ„ฐ ์กฐ๊ฑด์— ๋งž๋Š” ์ „๋ฌธ๊ฐ€ ๋ฆฌ์ŠคํŠธ ๊ฐ€์ ธ์˜ค๊ธฐ
    List<MemProAdInfoCate> pros = proService.getPaginatedFilteredAndSortedPros(
            appType, sort, itemNo, page, size);
    
    // ํ‰๊ท  ๊ฐ€๊ฒฉ ๊ณ„์‚ฐ
    double overallAveragePrice = calculateAveragePrice(pros);
    
    // ์‘๋‹ต ๊ตฌ์„ฑ
    Map<String, Object> response = new HashMap<>();
    response.put("pros", pros);
    response.put("hasMore", pros.size() == size);
    response.put("overallAveragePrice", 
            String.format("%,d", (long) overallAveragePrice));
    
    return response;
}

2. ์‹ค์‹œ๊ฐ„ ์ปค๋ฎค๋‹ˆ์ผ€์ด์…˜

  • WebSocket ๊ธฐ๋ฐ˜ ์‹ค์‹œ๊ฐ„ ํ†ต์‹ : STOMP ํ”„๋กœํ† ์ฝœ์„ ํ™œ์šฉํ•œ ์–‘๋ฐฉํ–ฅ ํ†ต์‹ 
  • ์ฑ„ํŒ…๋ฐฉ ๊ด€๋ฆฌ: ๊ฒฌ์  ๋ณ„ ์ฑ„ํŒ…๋ฐฉ ์ƒ์„ฑ, ๊ด€๋ฆฌ
  • ๋ฉ”์‹œ์ง€ ์ €์žฅ: ๋ชจ๋“  ๋Œ€ํ™” ๋‚ด์—ญ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ €์žฅ
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

    @Override
    public void configureMessageBroker(MessageBrokerRegistry config) {
        // ๋ฉ”์‹œ์ง€๋ฅผ ๊ตฌ๋…ํ•˜๋Š” prefix
        config.enableSimpleBroker("/topic", "/queue");
        // ๋ฉ”์‹œ์ง€๋ฅผ ๋ฐœํ–‰ํ•˜๋Š” prefix
        config.setApplicationDestinationPrefixes("/app");
    }

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/ws-stomp")
                .setAllowedOriginPatterns("*")
                .withSockJS();
    }
}

@MessageMapping("/chat/message")
public void sendMessage(@Payload ChatMessage message) {
    // ๋ฉ”์‹œ์ง€ ์ €์žฅ
    message.setSentAt(LocalDateTime.now());
    chatService.saveMessage(message);
    
    // ํŠน์ • ์ฑ„ํŒ…๋ฐฉ์œผ๋กœ ๋ฉ”์‹œ์ง€ ์ „์†ก
    messagingTemplate.convertAndSend(
            "/topic/chat/room/" + message.getRoomId(), message);
}

3. ์ฑ„ํŒ… ํ˜‘์—… ๋„๊ตฌ

  • ๊ฒŒ์‹œํŒ ๊ธฐ๋Šฅ: ์ฑ„ํŒ…๋ฐฉ ๋‚ด ๊ฒŒ์‹œ๊ธ€ ์ž‘์„ฑ ๋ฐ ๊ด€๋ฆฌ
  • ์ผ์ • ๊ด€๋ฆฌ: ํ”„๋กœ์ ํŠธ ์ผ์ • ๊ณต์œ  ๋ฐ ๊ด€๋ฆฌ (FullCalendar api)
  • ์‹ค์‹œ๊ฐ„ ์•Œ๋ฆผ: ๋ชจ๋“  ์•ก์…˜์— ๋Œ€ํ•œ ์‹ค์‹œ๊ฐ„ ์•Œ๋ฆผ
@PostMapping
public ResponseEntity<Map<String, Object>> createBoard(
        @RequestBody ChatBoardEvent board, HttpSession session) {
    Map<String, Object> response = new HashMap<>();
    
    chatBoardService.createBoard(board);
    
    Member member = (Member) session.getAttribute("member");
    
    // ์‹œ์Šคํ…œ ๋ฉ”์‹œ์ง€ ์ƒ์„ฑ
    ChatMessage systemMessage = new ChatMessage();
    systemMessage.setRoomId(board.getRoomId());
    systemMessage.setSender(member.getMemberId());
    systemMessage.setNickname(member.getNickname());
    systemMessage.setType("SYSTEM");
    systemMessage.setMessage(member.getNickname() + 
            "๋‹˜์ด ๊ฒŒ์‹œ๊ธ€์„ ์ž‘์„ฑํ•˜์˜€์Šต๋‹ˆ๋‹ค.");
    systemMessage.setSentAt(LocalDateTime.now());
    
    // DB์— ์‹œ์Šคํ…œ ๋ฉ”์‹œ์ง€ ์ €์žฅ
    chatService.saveMessage(systemMessage);
    
    // ์‹œ์Šคํ…œ ๋ฉ”์‹œ์ง€๋ฅผ ์›น์†Œ์ผ“์œผ๋กœ ์ „์†ก
    messagingTemplate.convertAndSend(
            "/topic/chat/room/" + board.getRoomId(), systemMessage);
    
    return ResponseEntity.ok(response);
}

4. ์นด์นด์˜ค/๊ตฌ๊ธ€ ์†Œ์…œ ๋กœ๊ทธ์ธ

  • OAuth2.0 ์ธ์ฆ: ์นด์นด์˜ค, ๊ตฌ๊ธ€ ์†Œ์…œ ๋กœ๊ทธ์ธ ์ง€์›
  • ๊ฐ„ํŽธํ•œ ํšŒ์›๊ฐ€์ž…: ์†Œ์…œ ๊ณ„์ •์œผ๋กœ ๊ฐ„ํŽธ ํšŒ์›๊ฐ€์ž…
  • ๋ณด์•ˆ ๊ฐ•ํ™”: ์•”ํ˜ธํ™”๋œ ๊ณ„์ • ๊ด€๋ฆฌ
@GetMapping("/callback")
public String kakaoCallback(
    @RequestParam(name = "code", required = true) String code,
    HttpSession session,
    Model model) {
    
    try {
        // 1. ์•ก์„ธ์Šค ํ† ํฐ ๋ฐ›๊ธฐ
        String accessToken = requestAccessToken(code);
        session.setAttribute("accessToken", accessToken);
        
        // 2. ์‚ฌ์šฉ์ž ์ •๋ณด ๋ฐ›๊ธฐ
        Map<String, Object> userData = requestUserInfo(accessToken);
        
        // 3. ์นด์นด์˜ค ID ์ถ”์ถœ
        String kakaoId = userData.get("id").toString();
        Map<String, Object> properties = 
                (Map<String, Object>) userData.get("properties");
        String nickname = (String) properties.get("nickname");
        
        // 4. ๊ธฐ์กด ํšŒ์›์ธ์ง€ ํ™•์ธ
        Member existingMember = memberService.getMember(kakaoId);
        
        if (existingMember != null) {
            // 5a. ๊ธฐ์กด ํšŒ์›์ด๋ฉด ๋กœ๊ทธ์ธ ์ฒ˜๋ฆฌ
            session.setAttribute("member", existingMember);
            session.setAttribute("isLogin", true);
            return "redirect:/mainPage";
        } else {
            // 5b. ์‹ ๊ทœ ํšŒ์›์ด๋ฉด ์ถ”๊ฐ€ ์ •๋ณด ์ž…๋ ฅ ํŽ˜์ด์ง€๋กœ
            model.addAttribute("kakaoId", kakaoId);
            model.addAttribute("nickname", nickname);
            return "member/kakaoAddJoinForm";
        }
    } catch (Exception e) {
        return "redirect:/loginForm?error=kakao";
    }
}

5. ๊ฒฐ์ œ ์‹œ์Šคํ…œ

  • ํฌํŠธ์› ๊ฒฐ์ œ ์—ฐ๋™: ์•ˆ์ „ํ•œ ๊ฒฐ์ œ ํ”„๋กœ์„ธ์Šค
  • ๊ฒฐ์ œ ๊ฒ€์ฆ: ์„œ๋ฒ„ ์‚ฌ์ด๋“œ ๊ฒฐ์ œ ๊ฒ€์ฆ
  • ๊ฒฐ์ œ ๋‚ด์—ญ ๊ด€๋ฆฌ: ์‚ฌ์šฉ์ž๋ณ„ ๊ฒฐ์ œ ๋‚ด์—ญ ๊ด€๋ฆฌ
public boolean verifyPayment(String impUid, int estimationNo, int paidAmount) 
        throws Exception {
    // 1. ์•ก์„ธ์Šค ํ† ํฐ ๋ฐœ๊ธ‰
    String accessToken = getAccessToken();

    // 2. ๊ฒฐ์ œ ์ •๋ณด ์กฐํšŒ API ํ˜ธ์ถœ
    RestTemplate restTemplate = new RestTemplate();
    String url = "https://api.iamport.kr/payments/" + impUid;
    
    HttpHeaders headers = new HttpHeaders();
    headers.set("Authorization", accessToken);
    HttpEntity<Void> entity = new HttpEntity<>(headers);
    
    ResponseEntity<String> response = 
            restTemplate.exchange(url, HttpMethod.GET, entity, String.class);
    
    // 3. ๊ฒฐ์ œ ๊ฒ€์ฆ
    if (response.getStatusCode() == HttpStatus.OK) {
        JSONObject responseBody = new JSONObject(response.getBody());
        JSONObject paymentData = responseBody.getJSONObject("response");
        
        double amount = paymentData.getDouble("amount");
        String status = paymentData.getString("status");
        
        // 4. DB์—์„œ ์ฃผ๋ฌธ ๊ธˆ์•ก ์กฐํšŒ
        double orderAmount = payMapper.getPrice(estimationNo);
        
        // 5. ๊ฒฐ์ œ ๊ฒ€์ฆ ๋กœ์ง
        if (amount == orderAmount && "paid".equals(status) && 
                paidAmount == amount) {
            return true;
        } else {
            throw new Exception("๊ฒฐ์ œ ๊ฒ€์ฆ ์‹คํŒจ: ๊ธˆ์•ก ๋˜๋Š” ์ƒํƒœ ๋ถˆ์ผ์น˜");
        }
    } else {
        throw new Exception("๊ฒฐ์ œ ์ •๋ณด ์กฐํšŒ ์‹คํŒจ");
    }
}

๐Ÿ— ์‹œ์Šคํ…œ ์•„ํ‚คํ…์ฒ˜

graph TB
subgraph Client
A[Web Browser]
end
subgraph "AWS Cloud"
subgraph "AWS EC2"
B[NGINX Proxy]
C[Spring Boot Application]
D[Jenkins]
end
subgraph "Storage"
E[Image Server]
end
subgraph "Database"
F[MySQL]
end
end
subgraph "External Services"
G[Kakao OAuth]
H[Google OAuth]
I[PortOne Payment]
J[Gmail SMTP]
end
A -->|HTTPS| B
B -->|Reverse Proxy| C
C -->|WebSocket| A
C -->|JDBC| F
C -->|File Upload| E
C <-->|OAuth2| G & H
C <-->|Payment API| I
C -->|Email| J
D -->|CI/CD| C
Loading

๐Ÿš€ ๋ฐฐํฌ ํ™˜๊ฒฝ

  • ์„œ๋ฒ„: AWS EC2 (Ubuntu 20.04 LTS)

  • ์›น ์„œ๋ฒ„: NGINX 1.18

  • ์ปจํ…Œ์ด๋„ˆํ™”: Docker & Docker Compose

  • CI/CD: Jenkins Pipeline

  • ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค: MySQL 8.0

  • ๋ชจ๋‹ˆํ„ฐ๋ง: Spring Actuator

  • ์ปจํ…Œ์ด๋„ˆํ™”: Docker ๋ฐ Docker Compose๋ฅผ ํ™œ์šฉํ•œ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์ปจํ…Œ์ด๋„ˆํ™”

# Dockerfile
# ๋นŒ๋“œ ๋‹จ๊ณ„
FROM gradle:8.12-jdk17-alpine AS build

WORKDIR /app

# ์˜์กด์„ฑ ๋จผ์ € ๋ณต์‚ฌ ๋ฐ ๋‹ค์šด๋กœ๋“œ (์บ์‹œ ํ™œ์šฉ)
COPY build.gradle settings.gradle ./
RUN gradle dependencies --no-daemon

# ์†Œ์Šค ๋ณต์‚ฌ ๋ฐ ๋นŒ๋“œ
COPY src ./src
RUN gradle build --no-daemon -x test

# ์‹คํ–‰ ๋‹จ๊ณ„
FROM amazoncorretto:17-alpine

WORKDIR /app

# MySQL ์„ค์ • ํŒŒ์ผ ์ƒ์„ฑ
RUN mkdir -p /etc/mysql/conf.d
RUN echo "[mysqld]" > /etc/mysql/conf.d/my.cnf
RUN echo "bind-address = 0.0.0.0" >> /etc/mysql/conf.d/my.cnf
RUN echo "skip-name-resolve" >> /etc/mysql/conf.d/my.cnf

# ํ•„์š”ํ•œ ํŒŒ์ผ๋งŒ ๋ณต์‚ฌ
COPY --from=build /app/build/libs/*.jar app.jar

ENV JAVA_OPTS="-Xms512m -Xmx512m"
ENV SERVER_PORT=8080

# MySQL ํฌํŠธ๋„ ๋…ธ์ถœ
EXPOSE 8080 3306

ENTRYPOINT ["sh", "-c", "java ${JAVA_OPTS} -jar app.jar"]
  • CI/CD: Jenkins๋ฅผ ํ™œ์šฉํ•œ ์ง€์†์  ํ†ตํ•ฉ ๋ฐ ๋ฐฐํฌ
 version: '3.8'

 services:
   db:
     image: mysql:8.0.41-bookworm
     container_name: myone
     ports:
       - "3306:3306"
     environment:
       MYSQL_ROOT_PASSWORD: Kyle9907!
       MYSQL_DATABASE: onestack
       MYSQL_USER: kyle
       MYSQL_PASSWORD: Kyle9907!
     networks:
       - onestack-network
     volumes:
       - mysql_data:/var/lib/mysql
       - ./src/main/resources/SQL:/docker-entrypoint-initdb.d
     command:
       - --character-set-server=utf8mb4
       - --collation-server=utf8mb4_unicode_ci
       - --default-authentication-plugin=mysql_native_password
     healthcheck:
       test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-pKyle9907!"]
       interval: 30s
       timeout: 10s
       retries: 5
       start_period: 60s
     restart: always

   app:
     build:
       context: .
       dockerfile: Dockerfile
     container_name: onestack
     ports:
       - "1234:8080"
     depends_on:
       db:
         condition: service_healthy
     volumes:
       - /usr/share/nginx/html/images:/usr/share/nginx/html/images
     networks:
       - onestack-network
     environment:
       SPRING_DATASOURCE_URL: jdbc:mysql://db:3306/onestack?useSSL=false&allowPublicKeyRetrieval=true&characterEncoding=UTF-8
       SPRING_DATASOURCE_USERNAME: kyle
       SPRING_DATASOURCE_PASSWORD: Kyle9907!
       KAKAO_CLIENT_ID: ${KAKAO_CLIENT_ID}
       KAKAO_REDIRECT_URI: ${KAKAO_REDIRECT_URI}
       GOOGLE_CLIENT_ID: ${GOOGLE_CLIENT_ID}
       GOOGLE_CLIENT_SECRET: ${GOOGLE_CLIENT_SECRET}
       GOOGLE_REDIRECT_URI: ${GOOGLE_REDIRECT_URI}
       GMAIL_USERNAME: ${GMAIL_USERNAME}
       GMAIL_PASSWORD: ${GMAIL_PASSWORD}
       PORTONE_API_KEY: ${PORTONE_API_KEY}
       PORTONE_API_SECRET: ${PORTONE_API_SECRET}
     restart: always

 networks:
   onestack-network:
     driver: bridge

 volumes:
   mysql_data:

๊ธฐ์ˆ ์  ์˜์‚ฌ๊ฒฐ์ •

1. WebSocket ๊ธฐ์ˆ  ์„ ํƒ

  • ๊ฒฐ์ • : Spring WebSocket + STOMP ํ”„๋กœํ† ์ฝœ ์ฑ„ํƒ
  • ์ด์œ  :
    • ์‹ค์‹œ๊ฐ„ ์–‘๋ฐฉํ–ฅ ํ†ต์‹  ์ง€์›
    • STOMP๋ฅผ ํ†ตํ•œ ๋ฉ”์‹œ์ง€ ๋ผ์šฐํŒ… ๋‹จ์ˆœํ™”
    • ํ™•์žฅ์„ฑ ๋ฐ ์„ฑ๋Šฅ ์ตœ์ ํ™”

2. MyBatis ORM ์ฑ„ํƒ

  • ๊ฒฐ์ • : JPA ๋Œ€์‹  MyBatis ์‚ฌ์šฉ
  • ์ด์œ  :
    • ๋ณต์žกํ•œ SQL ์ฟผ๋ฆฌ ์ง์ ‘ ์ž‘์„ฑ ๊ฐ€๋Šฅ
    • ๋†’์€ ์„ฑ๋Šฅ ๋ฐ ์ตœ์ ํ™” ์šฉ์ด
    • ์กฐ์ธ ์ฟผ๋ฆฌ ์ฒ˜๋ฆฌ ํšจ์œจ์ 

3. Docker ์ปจํ…Œ์ด๋„ˆํ™”

  • ๊ฒฐ์ • : Docker์™€ Docker Compose ํ™œ์šฉ
  • ์ด์œ  :
    • ๊ฐœ๋ฐœ/์šด์˜ ํ™˜๊ฒฝ ์ผ๊ด€์„ฑ ํ™•๋ณด
    • ๋ฐฐํฌ ์ž๋™ํ™” ์šฉ์ด
    • ์„œ๋น„์Šค ๋…๋ฆฝ์„ฑ ๋ฐ ํ™•์žฅ์„ฑ

๐Ÿ‘ฅ ํ”„๋กœ์ ํŠธ ๊ธฐ์—ฌ๋„

Team Leader

  • ๋ฐฑ์—”๋“œ ๊ฐœ๋ฐœ (๊ธฐ์—ฌ๋„ 80%)
  • ํ”„๋ก ํŠธ์—”๋“œ ๊ฐœ๋ฐœ (๊ธฐ์—ฌ๋„ 50%)
  • ์„œ๋น„์Šค ๊ธฐํš (๊ธฐ์—ฌ๋„ 85%)
  • ์ธํ”„๋ผ ๊ตฌ์ถ• ๋ฐ ๋ฐฐํฌ (๊ธฐ์—ฌ๋„ 100%)

๐Ÿ“ ๋ผ์ด์„ผ์Šค

์ด ํ”„๋กœ์ ํŠธ๋Š” MIT ๋ผ์ด์„ผ์Šค๋ฅผ ๋”ฐ๋ฆ…๋‹ˆ๋‹ค. ์ž์„ธํ•œ ๋‚ด์šฉ์€ LICENSE ํŒŒ์ผ์„ ์ฐธ์กฐํ•˜์„ธ์š”.

About

Global IT Final Project

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Contributors 6