a = np.array([-1,3, 2, 6])
b = np.array([3,6,1,2])

A = a.reshape([2,2])
print(A)
print(f"A.ndim:{A.ndim}") # 2차원
print(f"A.shape:{A.shape}") #2,2

B = np.reshape(b,[2,2])
print(B)

print(f"A+B\n:{A+B}")
print(f"A*B\n:{A*B}")
print(f"np.matmul:\n{np.matmul(A,B)}")
A		B
[[-1  3]	[[3 6]
 [ 2  6]]	 [1 2]]
A+B
:[[2 9]
 [3 8]]
 
A*B
:[[-3 18]
 [ 2 12]]
 
np.matmul:
[[ 0  0]
 [12 24]]
# matmul
결과 행렬 AB = A × B
각 원소의 계산 과정:
AB[0][0] = A[0][0]*B[0][0] + A[0][1]*B[1][0] = (-1)*3 + (3)*1 = 0
AB[0][1] = A[0][0]*B[0][1] + A[0][1]*B[1][1] = (-1)*6 + (3)*2 = 0
AB[1][0] = A[1][0]*B[0][0] + A[1][1]*B[1][0] = (2)*3 + (6)*1 = 12
AB[1][1] = A[1][0]*B[0][1] + A[1][1]*B[1][1] = (2)*6 + (6)*2 = 24

 

 

👉 왼쪽은 e, g (B의 왼쪽 세로줄),
👉 오른쪽은 f, h (B의 오른쪽 세로줄) 과 곱하면 됩니다.

'🟡데이터 분석 > numpy' 카테고리의 다른 글

5. numpy 기본 통계  (0) 2024.06.03
4. numpy 불린 연산  (1) 2024.06.03
03. numpy_기본연산 (arange)  (0) 2024.06.03
2. 인덱싱 슬라이싱  (0) 2024.06.03
1. array 생성하기 및 모듈 별명 지어주기  (0) 2024.06.03

🌈 목 차

  1. VPC, IGW, SUBNET, EC2(인스턴스) 생성
  2. VPC, IGW, SUBNET, EC2(인스턴스) 삭제
  3. MobaXterm – nginx 설치

 


 

🏢 VPC 개념을 아파트로 비유해서 쉽게 정리 (접은글)

더보기
개념 비유 설명
VPC (Virtual Private Cloud) 🏢 아파트 단지 AWS 안의 나만의 네트워크 공간
Subnet (서브넷) 🏬 아파트 동 (301동, 302동) VPC 안에서 공간을 나눈 구역 — 보통 공용(퍼블릭) / 내부(프라이빗) 용도로 구분
EC2 인스턴스 🚪 아파트 101호, 102호 실제 컴퓨터(서버) 역할 — 서브넷(동) 안에 위치
IGW (Internet Gateway) 🚪 아파트 단지의 출입문 인터넷으로 들어오고 나가는 통로
Routing Table (라우팅 테이블) 🪧 아파트 내 길안내판 데이터가 어느 길로 나가야 할지 안내
Security Group / NACL 🔒 보안 규칙 누가 들어올 수 있고, 나갈 수 있는지를 통제
Private IP ☎️ 집전화 내부 통신용 IP (VPC 내부에서만 사용 가능)
Public IP 📱 휴대폰 번호 외부 인터넷과 통신할 수 있는 IP (자동 변경 가능)
Elastic IP (EIP) 📞 고정 전화번호 변하지 않는 고정 Public IP — 항상 같은 IP로 접속 가능

리소스 테이블 정리

분류 이름 설명 비고
VPC EDU-VPC 10.250.0.0/16 65,563개 IP 사용 가능
Subnet EDU-PUBLIC-SBN-2A 10.250.1.0/24 가용 영역 : 2A(마북리)
Subnet EDU-PUBLIC-SBN-2C 10.250.11.0/24 가용 영역 : 2C(평촌)
       
분류 이름 설명
인터넷 게이트 웨이 EDU-IGW  
라우팅 테이블 EDU-PUBLIC-RT PUBLIC 라우팅 테이블은 인터넷 게이트웨이 사용하도록 지정합니다.
       
분류 이름 설명
Security Group EDU-PUBLIC-SG-2A 인바운드 규칙 80, 443, 22 Port
Security Group EDU-PUBLIC-SG-2C 인바운드 규칙 80, 443, 22 Port
EC2 EDU-PUBLIC-EC2-2A 10.250.1.240
EC2 EDU-PUBLIC-EC2-2C 10.250.11.240

 

 

🔝 목차로 가기

 


🌈 VPC, IGW, SUBNET, EC2(인스턴스) 생성

 

 


1. VPC 생성

테이블에 있는 표 작성

 

 

 

🔝 목차로 가기

 


2. 인터넷 게이트웨이(IGW) 생성 및 연결

 

2) 생성한 인터넷게이트웨이 선택후 VPC에 연결

 

3) 연결완료

 


 

3. VPC 설정 편집


 

🔝 목차로 가기

 

 

4. 서브넷(Subnet) 생성

 

서브넷1 생성

 

 

서브넷2 생성

 


 

 

🔝 목차로 가기

 

5. 라우팅 테이블 생성 및 서브넷 연결

 

라우팅 편집

 

서브넷 연결 편

 

 

🔝 목차로 가기

 


6. 보안규칙 생성

 

보안그룹(Security Group) 생성

 

 

 

 

 

 

🔝 목차로 가기

 

 

7. EC2 인스턴스 생성

7-1) 키페어 생성

EC2화면

 

키페어 생성

 

키페어 생성 > 다운로드 파일

 

 

 

7-2) 인스턴스 생성

 

 

 

 

기본 IP에 테이블에 있는 EC2 설명에 있는 아이피 붙이기

 

 

 

 

 

 

🔝 목차로 가기

 


 

8. 탄력적 IP 생성

퍼블릭IP주소가 매번 바뀌므로 탄력적 IP생성해야 고정됨

 

 

public-2A-EIP

public-2C-EIP

 

생성한 탄력적IP를 선택후 주소연결

 

 

 

🔝 목차로 가기


 

🌈 VPC, IGW, SUBNET, EC2(인스턴스) 삭제

삭제 순서 개요

1. 탄력적 IP 해제 및 삭제

  • 하나씩 선택-연결해제
  • 한꺼번에 선택 - 릴리즈 삭제

2. EC2 인스턴스 종료

3. RDS 삭제

4. 보안그룹 삭제

5. VPC 삭제

6. DHCP 옵션 세트 삭제


1. 탄력적 IP삭제 

-1)탄력적 IP주소 연결 해제 선택 (하나씩 선택 - 연결해제)

-2)릴리즈 삭제 (한꺼번에 선택)

 

 

탄력적 IP주소 연결 해제 선택 (하나씩 선택 - 연결해제)

 

 

-릴리즈 삭제 (한꺼번에 선택)

 


 

2. EC2 삭제 - 인스턴스 종료

 

 


 

3 . RDS삭제

-- 최종스냅샷 생성 해제

-- 자동백업 보존 해제

-- 마지막 1개 체크

 


4. 보안그룹 삭제

-- rds-ec2-1 : 인바운드 규칙 삭제

-- ec2-rds-1 : 아웃바운드 규칙 삭제

 

-- default를 제외한 나머지 삭제


 

5. VPC 삭제

(서브넷, 인터넷게이트웨이, 라우팅 테이블 자동 삭제)

 


 

6. DHCP 옵션 세트 삭제

 

 

🔝 목차로 가기


🌈 MobaXterm – nginx 설치

원격 접속(Remote host) 설정

 

 

Remote host : 탄력적 IP 작성

private key : 생성한 키페어 선택

상단 Bookmark setting : 이름 생성

 

 

 

 

nginx 설치 명령어

sudo : 관리자 권한 명령

cp : copy

vi : 편집

 

$ ping -c 5 탄력적IP
$ sudo apt update
$ sudo apt install -y nginx
$ sudo systemctl start nginx
$ sudo systemctl status nginx
$ git clone https://github.com/seoljinuk/ec2_homepage.git
$ cd ec*
$ ls
$ cd WebServer02
$ sudo cp index.html /usr/share/nginx/html/
$ sudo cp image01.png /usr/share/nginx/html/
$ sudo vi /etc/nginx/sites-available/default

// ESC I (INSERT)
root /usr/share/nginx/html/; (수정)
// ESC ; 
:wq -- 저장  
:q! -- 저장x 

$ sudo systemctl restart nginx

 

엔진 시작

 

 

 

 

 

 

🌈 데이터베이스 설치 – RDS(MySQL)


식별자  dbmysql80
마스터 사용자 이름 myroot
암호  (본인 암호 입력)
초기 데이터베이스 이름 shopping

 

 

 

 

 

데이터베이스 EC2에 연결 설정 명령어

$ sudo apt update
$ sudo apt install mysql-client -y
$ mysql -h [엔드포인트 붙여넣기] -P 3306 -u myroot -p
password:

>mysql(작성)

// ctrl + Z (mysql나가기)
$ cd
$ git clone https://github.com/seoljinuk/shopping_02_yes_database.git
$ ls
ec2_homepage  shopping_02_yes_database 

$ cd sh*
$ ls
$ cd src
$ cd main
$ cd resources
$ sudo vi application.properties

// 편집
url = localhost만 지우고 엔드포인트 붙여넣기
username = "작성"
userpassword ="작성"
// 저장 esc ;
:wq

$ cd
$ cd sho*
$ ls (pom.xml 위치한 곳에서 mvn메이븐 작성)
$ mvn package (test-compile 실행)

❗오류시 아래 코드 작성
---------------------------------------
$ sudo apt install maven
$ sudo apt install openjdk-17-jdk -y
$ sudo apt install -y maven
$ export JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64
$ mvn clean compile

내 PC 고급시스템 설정-> 환경변수 설정 -> jdk17-bin위치 설정 -> 시스템 변수 설정 -> 편집
---------------------------------------

(target 이동)
$ ls 
$ cd target 

-- port 9000으로 수정 
$ sudo iptables -t nat -A PREROUTING -p tcp --dport 80 -j REDIRECT --to-port 9000 
-- 실행 명령어
$ java -jar shopping-0.0.1-SNAPSHOT.jar

--> 탄력적IP주소 주소창에 입력후 이동 확인

 

더보기

sql작성문

use shopping ;
show tables ;

CREATE TABLE products (
    id BIGINT PRIMARY KEY,
    name VARCHAR(100),
    price INT,
    description TEXT,
    image_url VARCHAR(255)
);

INSERT INTO products (id, name, price, description, image_url) VALUES
(1, '카푸치노1', 4500, '맛있는 카푸치노', '/images/cappuccino01.png'),
(2, '크로아상2', 5000, '빵은 역시 크로아상^^', '/images/croissant_01.png'),
(3, '우유3', 3000, '따듯한 우유 조아요ㅎㅎ', '/images/milk01.jpg');

commit;

select * from products ;

 

🔝 목차로 가기

'🟡개발 도구 > AWS' 카테고리의 다른 글

aws  (0) 2024.01.18

1. 공공데이터 출력 (.csv)

package com.example.Ex02.TEST;

import java.io.*;

public class Ex01 {
    public static void main(String[] args) {
        File file =new File("file\\기상청27_관광코스별_관광지_지점정보.csv");
        BufferedReader br;
        String line;
        int count =0;
        try {
            br = new BufferedReader(new FileReader(file)); //연결 다리
            while((line = br.readLine()) != null) {
                count++;
                String [] arr = line.split(",");

                // 남해, 힐링 관광지 출력
                if (arr[4].contains("(남해)") && arr[10].contains("힐링")){
                    System.out.println(line);
                }
            }
            System.out.println("Count :" + count);
        } catch (FileNotFoundException e) {
            throw new RuntimeException(e);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}
package com.example.Ex02.TEST;

import java.io.*;

public class Ex02 {
    public static void main(String[] args) {

        // 강남구 도서관 출력
        File file =new File("file\\서울시 공공도서관 현황정보.csv");
        BufferedReader br;
        String line;

        try {
            br = new BufferedReader(new FileReader(file));
            while((line = br.readLine()) != null){
                String[]arr = line.split(",");
                if(arr[3].equals("\"강남구\"")){
                    System.out.println(line);
                }
            }

            br.close();
            br = new BufferedReader(new FileReader(file));

            // 어린이가 포함된 도서관 출력
            int count=0;
            while((line = br.readLine()) != null){
                String[]arr = line.split(",");
                if (arr[1].contains("어린이")) {
                    count++;
                    System.out.println(line);
                }
            }
            System.out.println("총 어린이 도서관 개수: " + count);
        } catch (FileNotFoundException e) {
            throw new RuntimeException(e);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}

 


2. 공공데이터 출력 ( json) 

 

pom.xml 입력

<dependency>
            <groupId>com.googlecode.json-simple</groupId>
            <artifactId>json-simple</artifactId>
            <version>1.1</version>
</dependency>

 

 

 

현재 위치 출력

public class Ex03 {
    public static void main(String[] args) throws IOException, ParseException, JSONException {

        Path currentPath = Paths.get("."); // 현재 위치
        String path = currentPath.toAbsolutePath().toString();
        System.out.println("path : " + path); // path : C:\Spring_study\Ex16\.

 

 

test1.json

{
"name" : "apple",    
"id" : 1,    
"price" : 1000
}
        System.out.println("=========Ex01=========");
        JSONParser parser = new JSONParser(); // Json (parser)파서기를 이용해 속성과 값을 분리
        Reader reader = new FileReader("file\\test1.json");
        JSONObject jsonObject = (JSONObject)parser.parse(reader);

        String name = (String) jsonObject.get("name");
        long id = (Long) jsonObject.get("id");
        long price = (Long) jsonObject.get("price");

        System.out.println("name :" + name);
        System.out.println("id :" + id);
        System.out.println("price :" + price);

        Set keySet = jsonObject.keySet();
        for (Object key : keySet) {
            System.out.println("key: " + key + ", value: " + jsonObject.get(key));
        }
}

 

test2.json

[
	{"name":"길동","age":"44"}, 
	{"name":"상철","age":"33"}
]
  System.out.println("=========Ex02=========");
        JSONParser parser2 = new JSONParser();
        Reader reader2 = new FileReader("file\\test2.json");
        Object jsonObject2 = parser2.parse(reader2);

        JSONArray jsonArr2 = (JSONArray) jsonObject2;
        System.out.println(jsonArr2.size());

        for (int i = 0; i < jsonArr2.size(); i++) {
            JSONObject jObject = (JSONObject)jsonArr2.get(i);
            Set keySet2 = jObject.keySet();
            for (Object key : keySet2) {
                System.out.println(key + " : " + jObject.get(key));
            }
        }

 

test3.json

{
	"id" : 1,
	"employee" : [
				{"name" : "윤아", "age":30 },
				{"name" : "웬디", "age":50 }
			],
	"company" :"메타 빌드"
}
System.out.println("=========Ex03=========");
        JSONParser parser3 = new JSONParser();
        Reader reader3 = new FileReader("file\\test3.json");
        JSONObject jsonObject3 = (JSONObject)parser3.parse(reader3);

        JSONArray jsonArr3  = (JSONArray)jsonObject3.get("employee");
        for (int i = 0; i < jsonArr3.size(); i++){
            JSONObject jObject3 = (JSONObject)jsonArr3.get(i);
            Set keySet3 = jObject3.keySet();
            for (Object key : keySet3){
                System.out.println(key +":"+jObject3.get(key));
            }

        }

 

💡 요약 한 줄

Ex01 = {} 단일 객체
Ex02 = [] 여러 객체 배열
Ex03 = { "키": [ ... ] } 객체 내부 배열

 

파일명  JSON 구조 파싱 객체  접근 방법
test1.json {} 단일 객체 JSONObject .get("키")
test2.json [] 배열 JSONArray for + get(i)
test3.json { "employee": [ ... ] } JSONObject → JSONArray get("employee") 후 반복

 

전체코드

더보기
package com.example.Ex02.TEST;

import org.json.simple.JSONArray;
import org.json.simple.JSONObject;
import org.json.simple.parser.JSONParser;
import org.json.simple.parser.ParseException;

import java.io.FileReader;
import java.io.IOException;
import java.io.Reader;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Set;

public class Ex03 {
    public static void main(String[] args) throws IOException, ParseException {

        // 현재 실행 경로 출력
        Path currentPath = Paths.get(".");
        String path = currentPath.toAbsolutePath().toString();
        System.out.println("path : " + path);

        // ------------------ Ex01: 단일 객체 ------------------
        System.out.println("=========Ex01=========");
        JSONParser parser = new JSONParser();
        Reader reader = new FileReader("file\\test1.json");
        JSONObject jsonObject = (JSONObject) parser.parse(reader);

        // 개별 key-value 접근
        System.out.println("name : " + jsonObject.get("name"));
        System.out.println("id : " + jsonObject.get("id"));
        System.out.println("price : " + jsonObject.get("price"));

        // 전체 key-value 출력
        for (Object key : jsonObject.keySet())
            System.out.println(key + " : " + jsonObject.get(key));


        // ------------------ Ex02: 배열 ------------------
        System.out.println("=========Ex02=========");
        JSONParser parser2 = new JSONParser();
        Reader reader2 = new FileReader("file\\test2.json");
        JSONArray jsonArr2 = (JSONArray) parser2.parse(reader2);

        // 배열 안의 각 객체 출력
        for (Object obj : jsonArr2) {
            JSONObject jObject = (JSONObject) obj;
            for (Object key : jObject.keySet())
                System.out.println(key + " : " + jObject.get(key));
        }


        // ------------------ Ex03: 객체 내부 배열 ------------------
        System.out.println("=========Ex03=========");
        JSONParser parser3 = new JSONParser();
        Reader reader3 = new FileReader("file\\test3.json");
        JSONObject jsonObject3 = (JSONObject) parser3.parse(reader3);

        // "employee" 배열 추출 후 반복 출력
        JSONArray jsonArr3 = (JSONArray) jsonObject3.get("employee");
        for (Object obj : jsonArr3) {
            JSONObject jObject3 = (JSONObject) obj;
            for (Object key : jObject3.keySet())
                System.out.println(key + " : " + jObject3.get(key));
        }
    }
}

 

3. 공공데이터 출력 ( json) 

https://git-scm.com/downloads/win

 

Git - Downloading Package

Download for Windows Click here to download the latest (2.51.0(2)) x64 version of Git for Windows. This is the most recent maintained build. It was released on 2025-09-29. Other Git for Windows downloads Standalone Installer Git for Windows/x64 Setup. Git

git-scm.com


Git Bash를 항상 관리자 권한으로 실행

 

  • “Git Bash” 아이콘 우클릭 → “속성”
  • “호환성” 탭 → “관리자 권한으로 이 프로그램 실행” 체크

 


bash에서 cd/c/..파일 경로를 통해 들어가기

cd /c/Java_ljr

 

다음 단계 (GitHub 연결)

git init
git remote add origin https://github.com/jeongryuni/Study_Java.git
git add .
git commit -m "first commit"
git branch -M main
git push -u origin main

 

 

gitignore생성

https://www.toptal.com/developers/gitignore

 

gitignore.io

Create useful .gitignore files for your project

www.toptal.com

.gitignore

기본 준비

server.port = 8080
    spring.datasource.url=jdbc:oracle:thin:@localhost:1521:orcl
    spring.datasource.username=sqlid
    spring.datasource.password=sqlpw
    spring.datasource.driver-class-name=oracle.jdbc.OracleDriver

mybatis.mapper-locations=classpath:mapper/*.xml

spring.jpa.hibernate.ddl-auto=create 
spring.jpa.hibernate.ddl-auto=update 
spring.jpa.properties.hibernate.show_sql=true
spring.jpa.properties.hibernate.format_sql=true

 

pom.xml 추가

properties 아래 추가
<repositories>
        <repository>
            <id>oracle</id>
            <url>https://repo.maven.apache.org/maven2</url>
        </repository>
</repositories>

<dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>3.0.5</version>
</dependency>
<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
            <groupId>nz.net.ultraq.thymeleaf</groupId>
            <artifactId>thymeleaf-layout-dialect</artifactId>
</dependency>

 

새로 추가 (layout, ModelMapper)

<dependency>
        <groupId>nz.net.ultraq.thymeleaf</groupId>
        <artifactId>thymeleaf-layout-dialect</artifactId>
</dependency>
<dependency>
            <groupId>org.modelmapper</groupId>
            <artifactId>modelmapper</artifactId>
            <version>2.3.9</version>
</dependency>

 

Controller → Service → Repository → Entity → View(Thymeleaf)

 


📁common

 

header.html

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">

    <meta charset="UTF-8">
    <div th:fragment="top">
        <nav class="navbar navbar-expand-sm navbar-dark bg-dark">
            <div class="navbar-brand">
                <a th:href="'/'" style="color:white">도서 정보 페이지</a>
            </div>
        </nav>
    </div>
</html>

 

footer.html

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
    <meta charset="UTF-8">

    <div class="footer" th:fragment="bottom">
        <footer class="page-footer font-small cyan darken-3">
            <div class="footer-copyright text-center py-3">
                Book WebSite
            </div>
        </footer>
    </div>
</html>

 

mylayout.html

<!DOCTYPE html>
<html lang="en"
      xmlns:th="http://www.thymeleaf.org"
      xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
>
<head>
    <meta charset="UTF-8">
    <title>mylayout</title>
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css">
    <link th:href="@{/css/mylayout.css}" rel="stylesheet">
    <script src="https://code.jquery.com/jquery-3.5.1.min.js"></script>

    <th:block layout:fragment="script"></th:block>
    <th:block layout:fragment="css"></th:block>
</head>
<body>
<div th:replace="~{common/header::top}"></div>
<div layout:fragment="content"></div>
<div th:replace="~{common/footer::bottom}"></div>
</body>
</html>

📁templates/book

1. 기본 구조

<!DOCTYPE html>
<html lang="en"
      xmlns:th="http://www.thymeleaf.org"
      xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
      layout:decorate="~{common/mylayout}">

 

  • xmlns:th : Thymeleaf 네임스페이스 (th:속성 사용 가능)
  • xmlns:layout : Layout Dialect 사용을 위한 네임스페이스
  • layout:decorate : common/mylayout.html 템플릿을 기반으로 현재 페이지 내용을 삽입
    → 즉, 이 페이지는 공통 레이아웃을 상속받는 자식 페이지

 


select.html

더보기
<!DOCTYPE html>
<html lang="en"
      xmlns:th="http://www.thymeleaf.org"
      xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
      layout:decorate="~{common/mylayout}">
>
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <div class="content" layout:fragment="content">
        <h2>도서 목록 페이지</h2>
        <form th:action="@{/blist}" method="get" class="form-inline mb-3 justify-content-center">
            <div class="form-group mx-sm-3 mb-2">
                <input type="text" name="keyword" th:value="${keyword}" class="form-control" placeholder="제목 또는 저자입력">
                <button type="submit" class="btn btn-primary mb-2">검색</button>
            </div>
        </form>
        <table class="table">
            <thead>
                <tr>
                    <th>번호</th>
                    <th>제목</th>
                    <th>저자</th>
                    <th>출판사</th>
                    <th>가격</th>
                    <th>입고일</th>
                    <th>배송비</th>
                    <th>구매가능서점</th>
                    <th>보유수량</th>
                    <th>수정</th>
                    <th>삭제</th>
                </tr>
            </thead>

            <tbody>
                <tr th:each="book : ${bookList}">
                    <td th:text="${book.no}"></td>
                    <td th:text="${book.title}"></td>
                    <td th:text="${book.author}"></td>
                    <td th:text="${book.publisher}"></td>
                    <td th:text="${book.price}"></td>
                    <td th:text="${book.buy}"></td>
                    <td th:text="${book.kind}"></td>
                    <td th:text="${book.bookstore}"></td>
                    <td th:text="${book.count}"></td>
                    <td>수정</td>
                    <td>삭제</td>
                </tr>
            </tbody>
        </table>

        <div style="text-align:center">
            <button type="button" class="btn btn-primary"
                    th:onclick="|location.href='@{/book/insert}'|">추가</button>
            <button type="button" class="btn btn-danger">선택항목 삭제</button>
        </div>
        <br>
        <!--페이지 설정-->
        <!--0 이상일때-->
        <div th:if="${bookList.totalPages > 0}">
            <nav>
                <ul class="pagination justify-content-center">
                    <li class="page-item" th:classappend="${bookList.hasPrevious()}?'':'disabled'">
                        <a class="page-link" th:href="@{/blist(page=${bookList.number-1}, size=${bookList.size}, keyword=${keyword})}">Previous</a>
                    </li>

                    <li class="page-item"
                        th:each="i : ${#numbers.sequence(0, bookList.totalPages-1)}" th:classappend="${i == bookList.number} ? 'active' : ''">
                        <a class="page-link"
                           th:href="@{/blist(page=${i}, size=${bookList.size}, keyword=${keyword})}" th:text="${i + 1}">
                        </a>
                    </li>

                    <li class="page-item" th:classappend="${bookList.hasNext()}?'':'disabled'">
                        <a class="page-link" th:href="@{/blist(page=${bookList.number+1}, size=${bookList.size}, keyword=${keyword})}">Next</a>
                    </li>

                </ul>
            </nav>
        </div>
    </div>
</body>
</html>

 

 


 

📁BookBean

public class BookBean {
    private int no;

    @NotBlank(message = "제목 필수 입력")
    private String title;
    @NotBlank(message = "저자 필수 입력")
    private String author;
    @NotBlank(message = "출판사 필수 입력")
    private String publisher;

    @NotNull(message = "가격을 입력하세요")
    @Min(value = 1, message = "가격은 1원 이상이어야 합니다.")
    private int price;
    @NotEmpty(message = "입고일 필수 입력")
    private String buy;
    @NotEmpty(message = "배송비 필수 입력")
    private String kind;
    @NotEmpty(message = "구입가능 서점 하나 이상 선택")
    private String bookstore;
    @Min(value = 1, message = "보유수량은 1개 이상이어야 합니다.")
    private int count;

📁Entity

BookEntity

@Entity
@Table(name = "book")
public class BookEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "book_seq_gen")
    @SequenceGenerator(
            name = "book_seq_gen",  // 시퀀스 제너레이터 이름
            sequenceName = "BOOK_SEQ",  // 실제 DB 시퀀스 이름
            allocationSize = 1  // 증가 단위 (보통 1)
    )
    private int no;
    private String title;
    private String author;
    private String publisher;
    private int price;
    private String buy;
    private String kind;
    private String bookstore;
    private int count;

 

  • @Entity : JPA가 관리하는 엔티티 클래스
  • @Table(name="book") : 실제 DB 테이블 이름 명시
  • @Id : 기본키 지정
  • @GeneratedValue + @SequenceGenerator : Oracle 시퀀스로 PK 자동 증가

📁Controller

BookController

더보기
@Controller
public class BookController {

    // (1) BookService를 주입받음 (비즈니스 로직 처리 담당)
    @Autowired
    BookService bookService;

    // [1️⃣ 도서 목록 조회 + 검색 + 페이징]
    @GetMapping(value = {"/", "blist"}) // (2) URL의 쿼리 파라미터를 받음
    public String getAllBooks(
            @RequestParam(defaultValue = "0") int page,
            @RequestParam(defaultValue = "3") int size,
            @RequestParam(required = false) String keyword,
            Model model
    ){

        // (3) DB의 전체 도서 수 조회
        long totalCount = bookService.count();
        System.out.println("totalCount : " + totalCount);


        // (4) Service를 통해 현재 페이지에 표시할 도서 목록을 가져옴 :  Page Class
        Page<BookEntity> bookList = bookService.getBookEntity(page,size,keyword); // 현재 페이지에 출력할 3가지

        // (5) 디버깅용 로그 출력 (페이징 정보)
        System.out.println("bookList.getTotalElements() :" +bookList.getTotalElements()); //전체 데이터 개수
        System.out.println("bookList.getTotalPages() :" +bookList.getTotalPages()); // 전체 페이지 수
        System.out.println("bookList.getNumber() :" + bookList.getNumber()); // 현재 페이지 번호(0부터 시작)
        System.out.println("bookList.getNumberOfElements()" + bookList.getNumberOfElements());

        // (6) 화면(view)에 전달할 데이터 등록
        model.addAttribute("bookList", bookList);
        model.addAttribute("page", page);
        model.addAttribute("size", size);
        model.addAttribute("keyword", keyword);
        model.addAttribute("totalCount", totalCount);

        return "book/select";
    }

    // [2️⃣ 도서 등록 폼으로 이동]
    @GetMapping(value = "book/insert")
    public String insertBook(Model model){
        model.addAttribute("bookBean", new BookBean());
        return "book/insert";
    }

    // [3️⃣ 도서 등록 처리]
    @PostMapping(value = "/book/insert")
    public String insertBookProc(@Valid@ModelAttribute("bookBean") BookBean bookBean, BindingResult br, Model model){

        if(br.hasErrors()){
            return "/book/insert";
        }
        //bookBean을 Entity로 바꾸는 작업
        BookEntity bookEntity = bookService.beanToEntity(bookBean);
        bookService.saveBook(bookEntity);
        return "redirect:/blist";
    }
}

🔽bookBean을 Entity로 바꾸는 작업을 하는 이유 ==> Service에서 작업

BookEntity bookEntity = bookService.beanToEntity(bookBean);
bookService.saveBook(bookEntity);

1️⃣ 배경: Bean과 Entity의 역할이 다름

  • BookBean 은 폼과 화면용
  • BookEntity 는 데이터베이스 저장용

2️⃣ save() 메서드는 BookEntity만 저장할 수 있음

👉 따라서 Bean → Entity 변환 과정이 필요


📁Service

BookService

 📘 [BookService 클래스 설명]
 Controller ↔ Service ↔ Repository 사이에서 데이터를 주고받는 중간 다리 역할
 - Controller가 요청을 보내면 Service가 로직을 처리하고
 - Repository를 통해 DB와 실제로 데이터를 주고받는다.
 

ModelMapper 객체 생성 - Bean ↔ Entity 간 자동 변환을 도와주는 라이브러리

더보기
package com.example.Ex02.service;

/*controller - BookService - repository :중간 연결다리*/

import com.example.Ex02.bean.BookBean;
import com.example.Ex02.entity.BookEntity;
import com.example.Ex02.repository.BookRepository;
import org.modelmapper.ModelMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service;

@Service
public class BookService {
    @Autowired
    BookRepository bookRepository;

    // ModelMapper : Bean ↔ Entity 자동 변환 도구
    private static ModelMapper modelMapper = new ModelMapper();

    // 1️⃣ 도서 총 개수 조회
    public long count() {
        return bookRepository.count();
    }

    // 2️⃣ 도서 목록 조회 (검색 + 페이징)
    // Pageable : 페이징 정보와 정렬 기준 지정 (no 기준 내림차순)

    public Page<BookEntity> getBookEntity(int page, int size, String keyword) {
        Pageable pageable = PageRequest.of(page,size, Sort.by("no").descending());

        // 검색어(keyword)가 없으면 전체 목록 반환
        if (keyword == null || keyword.isEmpty()){
            return bookRepository.findAll(pageable);
        } 
        // 검색어가 있으면 제목 또는 저자에 keyword가 포함된 결과만 반환
        else{
            return bookRepository.findByTitleContainingOrAuthorContaining(keyword, keyword, pageable);
        }
    }

    // 3️⃣ Bean → Entity 변환
    public BookEntity beanToEntity(BookBean bookBean) {
        BookEntity bookEntity= modelMapper.map(bookBean, BookEntity.class);
        return bookEntity;
    }
    // 4️⃣ 도서 저장 (등록 또는 수정)
    public void saveBook(BookEntity  bookEntity){
        bookRepository.save(bookEntity);
    }

}

 

📁Repository (인터페이스)

Controller  →  Service  →  Repository(interface)  →  DB

public interface BookRepository extends JpaRepository<BookEntity, Integer> {

    Page<BookEntity> findByTitleContainingOrAuthorContaining(String keyword, String keyword1, Pageable pageable);
}

🧩 Thymeleaf 헤더(Fragment)로 로그인 상태 표시하기

Spring Boot + Thymeleaf를 사용할 때,
모든 페이지에 공통으로 들어가는 상단 헤더 영역(header)
th:fragment를 이용해서 한 번만 작성하고 여러 페이지에서 재사용 가능

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<div th:fragment="header">
    <!--loginInfo가 null이 아니면 id, null이면 '로그인이 필요합니다.'-->
    로그인 : <span th:text="${session.loginInfo != null? session.loginInfo.id : '로그인이 필요합니다.' }"></span>
    <br>
    <a th:href = "@{/}">시작페이지</a>
    <a th:href = "@{/logout.mb}">로그아웃</a>
</div>
</body>
</html>

🧱  다른 페이지에서 사용하기

<!-- header.html의 header fragment 불러오기 -->
<div th:replace="~{header :: header}"></div>

 

로그인을 안했을경우
로그인을 했을경우

🌈Spring Boot JPA + Lombok 엔티티 매핑 실습

(자동 테이블 생성, CRUD)


🌈1. DB 설정 + JPA 설정(application.properties)

 

 

 application.properties 에 작성

// DB연결 설정
spring.datasource.url=jdbc:oracle:thin:@localhost:1521:orcl
spring.datasource.username=sqlid
spring.datasource.password=sqlpw
spring.datasource.driver-class-name=oracle.jdbc.OracleDriver

 

🔽 실행시 테이블 새로 자동생성

// JPA 테이블 생성
spring.jpa.hibernate.ddl-auto=create
spring.jpa.properties.hibernate.show_sql=true
spring.jpa.properties.hibernate.format_sql=true

 

🔽 테이블을 시작할때마다 재생성을 안하고 업데이트 하기위함

➡️ 재생성을 안할시 create를 지우고 update로 수정

spring.jpa.hibernate.ddl-auto=update
spring.jpa.properties.hibernate.show_sql=true
spring.jpa.properties.hibernate.format_sql=true

 

JPA 설정

  • ddl-auto=create → 실행 시 테이블을 새로 자동 생성 (데이터 초기화됨)
  • show_sql=true → 실행되는 SQL 문장을 콘솔에서 확인 가능
  • format_sql=true → SQL 문장을 보기 좋게 줄바꿈/들여쓰기 출력

🌈2. 파일 경로 및 설정

 


 

🌈3. 클래스 설정 

Lombok 어노테이션

@NoArgsConstructor 매개변수가 없는 기본 생성자 자동 생성
@AllArgsConstructor 모든 필드를 매개변수로 받는 전체 생성자 자동 생성

 

 JPA 어노테이션

@Entity 클래스 이름을 보고 테이블 자동생성
@Id PK(Primary Key) 지정. @Entity가 붙은 클래스에는 반드시 하나 필요
@Table(name = 'e_test' ) 테이블 이름 새로 지정
@Column(name = "irum", nullable = false) 매핑될 컬럼 이름 및 제약조건 지정 (컬럼 이름: irum, not null)

 

 

 

🗂️entity

➡️📁EntityTest.java

package com.example.Ex02.entity;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.AllArgsConstructor;
import lombok.NoArgsConstructor;

@NoArgsConstructor // 매개변수가 없는 생성자
@AllArgsConstructor // 매개변수가 모두 있는 생성자
@Table(name = "eTest") // 테이블 이름 새로 지정
@Entity // 클래스 이름을 보고 테이블 자동생성 
public class EntityTest {
    @Id // PK(Primary Key) 지정 @Entity가 붙은 클래스에는 반드시 하나 필요

    private int num;

    @Column(name = "irum", nullable = false)
    private String name;

    private String addr;
    private int age;

    public int getNum() {
        return num;
    }

    public void setNum(int num) {
        this.num = num;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getAddr() {
        return addr;
    }

    public void setAddr(String addr) {
        this.addr = addr;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}

🌈4. 인터페이스 설정

package com.example.Ex02.repository;

import com.example.Ex02.entity.EntityTest;
import org.springframework.data.jpa.repository.JpaRepository;

// JpaRepository<엔티티 클래스, 프라이머리키(PK) 타입>
// EntityTest 엔티티를 관리하고, PK 타입은 int 이므로 Integer 사용
public interface EntityRepository extends JpaRepository<EntityTest, Integer> {
}

 

✅ 특징

  1. JpaRepository<EntityTest, Integer>
    • 첫 번째 제네릭: 엔티티 클래스 (EntityTest)
    • 두 번째 제네릭: PK 타입 (int → Integer)
  2. JpaRepository 를 상속했기 때문에 CRUD 기능이 자동으로 제공
    • JpaRepository<T, ID>CrudRepository<T, ID>PagingAndSortingRepository<T, ID> 를 상속
    • 그래서 save, findById, findAll, deleteById 같은 CRUD 메서드가 기본 내장

🌈5. EntityTest 엔티티를 저장(insert) - Save() 

@SpringBootTest가 import가 안될때

더보기
더보기

spring-boot-starter-test 에서 scope주석 처리

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <!--<scope>test</scope>-->
</dependency>

 

@SpringBootTest
public class EntityRepositoryTest {

    @Autowired
    EntityRepository entityRepository;

    @Test
    @DisplayName("entity 테스트")
    public void entitySave(){
        EntityTest etest = new EntityTest();
        etest.setNum(100);
        etest.setName("ljr");
        etest.setAddr("경기");
        etest.setAge(20);

        entityRepository.save(etest);
    }
}

 

🔽실행방법 (메서드만 실행)

1. 왼쪽 메서드 실행 버튼

 

2. 마우스 우클릭 후 Run 'entitySave()'

 

 

 

➡️JpaRepository 의 save() 메서드는 반드시 엔티티(Entity) 객체만 받을 수 있습니다.

 


🌈6. EntityTest 엔티티를 조회(select) - findAll() / findByName()

 

6-1) 전체 조회 findAll()

- toString으로 출력시 dto에서 toString 오버라이딩

@Test
@DisplayName("10개 레코드 삽입")
public void e_testSave(){
    String[] irum = {"윤아", "아이유", "웬디", "민호"};
    String[] addr = {"서울", "제주", "부산"};
    int[] age = {10, 20, 30, 40};

    for(int i=1; i<=10; i++){
        EntityTest etest = new EntityTest();
        etest.setNum(i);
        etest.setName(irum[i % irum.length]); // 1 2 3 0 1 2 나눠서 나머지 결과값으로 반복작업
        etest.setAddr(addr[i % addr.length]);
        etest.setAge(age[i % age.length]);
        entityRepository.save(etest);
    }
}

@Test
@DisplayName("전체 레코드 조회")
public void findAllTest(){
    List<EntityTest> elists = entityRepository.findAll(); //전체 레코드 조회 : List 리턴
    for(EntityTest entity :elists){
/*            System.out.println(entity.getNum());
        System.out.println(entity.getName());
        System.out.println(entity.getAddr());
        System.out.println(entity.getAge());*/
        System.out.println(entity.toString()); // 메서드를 오버라이딩 하지않을시 => 주소출력, dto에서 오버라이딩
        System.out.println("================");
    }
}

 

 

6-2) 특정 레코드 조회 - findByName()

@Test
@DisplayName("이름이 '윤아'인 레코드 조회")
public void findName() {
    List<EntityTest> elists = entityRepository.findByName("윤아");
    for(EntityTest entity : elists){
        System.out.println(entity.toString());
    }
}

@Test
@DisplayName("이름에 '수'가 포함된 이름 조회")
public void findNameFind(){
    List<CompanyEntity> lists = companyRepository.findByNameContaining("수");

    for(CompanyEntity cEntity : lists) {
        System.out.println(cEntity.toString());
    }
}

@Test
@DisplayName("이름에 '수'가 포함된 이름 조회 & 회사명 내림차순 조회")
public void findNameAndCompanyFind(){
    List<CompanyEntity> lists = companyRepository.findByNameContainingOrderByCompanyDesc("수");

    for(CompanyEntity cEntity : lists) {
        System.out.println(cEntity.toString());
    }
}

@Test
@DisplayName("나이가 30인 레코드 조회")
public void findAge() {
    List<EntityTest> elists = entityRepository.findByAge(30);
    for(EntityTest entity : elists){
        System.out.println(entity.toString());
    }
}

@Test
@DisplayName("나이가 30 초과인 레코드 조회")
public void findAgeOver() {
    List<EntityTest> elists = entityRepository.findByAgeGreaterThan(30);
    for(EntityTest entity : elists){
        System.out.println(entity.toString());
    }
}

@Test
@DisplayName("나이가 30 이상인 레코드 조회")
public void findAgeEqual() {
    List<EntityTest> elists = entityRepository.findByAgeGreaterThanEqual(30);
    for(EntityTest entity : elists){
        System.out.println(entity.toString());
    }
}

 

인터페이스

 

// JpaRepository<EntityTest, Integer> 
// → 첫 번째 제네릭(EntityTest): 엔티티 클래스
// → 두 번째 제네릭(Integer): 프라이머리키(기본키) 타입
public interface EntityRepository extends JpaRepository<EntityTest, Integer> {

    // 1. name 컬럼이 특정 값과 일치하는 레코드 조회
    List<EntityTest> findByName(String name);

    // 2. age 컬럼이 특정 값과 일치하는 레코드 조회
    List<EntityTest> findByAge(int i);

    // 3. age 컬럼이 특정 값보다 큰 레코드 조회 (>)
    List<EntityTest> findByAgeGreaterThan(int i);

    // 4. age 컬럼이 특정 값 이상인 레코드 조회 (>=)
    List<EntityTest> findByAgeGreaterThanEqual(int i);
    
    
    
    List<CompanyEntity> findBySalaryGreaterThanEqual(int i);

    List<CompanyEntity> findByNameContaining(String 수);

    List<CompanyEntity> findByNameContainingOrderByCompanyDesc(String 수);
    
}

 

  1. 메서드 네이밍 규칙 (쿼리 메서드)
    • findBy필드명 → 해당 필드 조건으로 조회
    • findBy필드명And필드명 → 여러 조건 조회
    • findBy필드명GreaterThan → > 조건
    • findBy필드명GreaterThanEqual → >= 조건
    • findBy필드명LessThan, Between, Like, In 등도 가능
  2. 자동으로 JPQL 생성됨
    • 예) findByName("윤아")
      → 실행 SQL: select * from entity_test where name = '윤아';
    • 예) findByAgeGreaterThan(20)
      → 실행 SQL: select * from entity_test where age > 20;

 

6-3) id조회 (primarykey)

@Test
@DisplayName("id 조회")
// PK가 num인 경우 findById()와 findByNum()은 결과가 동일하다. 하지만 findById()는 PK 전용
public void getId(){
    Optional<ItemEntity> o = itemRepository.findById(50L);
    System.out.println("o" + o);
}

 

➡️findById()Optional을 리턴

➡️PK가 Long 타입 이므로 findById(50L) 메서드 안 숫자에 'L' 기입

 

6-4) 가격 조회(primarykey)

@Test
@DisplayName("6. price조회")
// price 300보다 작은 레코드를 가격기준 내림차순 정렬 조회 출력
// Query method
public void getPrice(){
    List<ItemEntity>lists = itemRepository.findByPriceLessThanOrderByPriceDesc(300);
    for (ItemEntity item:lists){
        System.out.println(item);
    }
}

@Test
@DisplayName("7. price조회")
// price 300큰 레코드를 가격기준 오름차순 정렬 조회 출력
// Query method
public void getPrice2(){
    List<ItemEntity>lists = itemRepository.findByPriceGreaterThanOrderByPriceAsc(300);
    for (ItemEntity item:lists){
        System.out.println(item);
    }
}

 

 

인터페이스

public interface ItemRepository extends JpaRepository<ItemEntity, Long> {

    List<ItemEntity> findByItemNm(String ss);

    List<ItemEntity> findByPriceLessThanOrderByPriceDesc(int i);

    List<ItemEntity> findByPriceGreaterThanOrderByPriceAsc(int i);
}

 

🌈7. 쿼리 어노테이션 (JPQL)

//쿼리 어노테이션
@Test
@DisplayName("Query annotation으로 전체레코드 조회")
public void allSelect(){
    List<CompanyEntity> lists = companyRepository.findAllQuery();
    for(CompanyEntity cEntity : lists) {
        System.out.println(cEntity.toString());
    }
}

@Test
@DisplayName("전체 레코드 개수조회")
public void countCompany(){
    int count = companyRepository.countCompany();
    System.out.println("➡️count :" + count);
}

@Test
@DisplayName("이름이 '수지'인 레코드 조회")
public void findByNameSuji(){
    List<CompanyEntity> lists = companyRepository.findByNameSuji("수지");
    for(CompanyEntity cEntity : lists) {
        System.out.println(cEntity.toString());
    }
}

@Test
@DisplayName("이름에 '수'가 포함된 레코드 조회")
public void findByNameSujiContainQuery(){
    List<CompanyEntity> lists = companyRepository.findNameContainSu("수");
    for(CompanyEntity cEntity : lists) {
        System.out.println(cEntity.toString());
    }
}

@Test
@DisplayName("상세설명에 '어' 포함된 레코드 조회")
public void ItemDetailFind(){
    List<ItemEntity>lists = itemRepository.findByItemDetail("어");
    for (ItemEntity item:lists){
        System.out.println(item);
    }
}

@Test
@DisplayName("상세설명에 '어' 포함 & 가격이 300이상 레코드 조회")
public void ItemDetailPriceFind(){
    List<ItemEntity>lists = itemRepository.findByItemDetailPrice("어", 300);
    for (ItemEntity item:lists){
        System.out.println(item);
    }
}

@Test
@DisplayName("7. 레코드 삭제")
public void deleteId(){
    itemRepository.deleteById(1L);
    }

 

인터페이스

nativeQuery = true : 테이블명 작성
nativeQuery = false : 클래스명 작성
package com.example.Ex02.repository;


import com.example.Ex02.entity.CompanyEntity;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;

import java.util.List;
import java.util.Optional;

public interface CompanyRepository extends JpaRepository<CompanyEntity, Integer> {

    // 1. 전체 조회
    /*@Query(value = "select * from company_entity", nativeQuery = true)*/
    @Query(value = "select i from CompanyEntity i", nativeQuery = false)
    List<CompanyEntity> findAllQuery();

    // 2. 레코드 개수 조회
    @Query("select count(company) from CompanyEntity company")
    int countCompany();
    
    // 3. 이름이 '수지'인 사람 조회
    @Query("SELECT i FROM CompanyEntity i WHERE i.name = :name")
    List<CompanyEntity> findByNameSuji(@Param("name") String name);
    
    // 4. 이름에 '수'가 포함된 사람조회 & 회사이름 내림차순
    @Query("SELECT i FROM CompanyEntity i WHERE i.name LIKE %:name% order by company desc")
    List<CompanyEntity> findNameContainSu(@Param("name") String name);
    
    // 5. 상세설명에 '어'들어간 레코드 찾기
    @Query(value = "select i from ItemEntity i where i.itemDetail like %:itemDetail%")
    List<ItemEntity> findByItemDetail(@Param("itemDetail")String itemDetail);

    // 6. 상세설명에 '어'포함 & price가 300이상인 레코드 조회
    @Query(value = "select i from ItemEntity i where i.itemDetail like %:itemDetail% and i.price >= :price")
    List<ItemEntity> findByItemDetailPrice(@Param("itemDetail, price") String itemDetail, int price);
    
    //8. 특정 레코드 삭제
    @Modifying
    @Transactional
    @Query(value = "delete from item_entity where item_name = :itemname", nativeQuery = true)
    void deleteItemName(@Param("itemname") String itemName);
}

 

delete를 할때 두개의 어노테이션이 필요한 이유

1️⃣ @Modifying — “SELECT 아님!” 표시

2️⃣ @Transactional — : “commit / rollback” 보장

: 삭제가 되면 커밋, 안되면 롤백

데이터를 바꾸는(delete/update/insert) 모든 쿼리는 트랜잭션 안에서만 안전하게 실행

 

 

 

<td th:text="${#numbers.formatDecimal(product.price, 0, 'COMMA', 0, 'POINT')}"></td>

 

설명

  • 첫 번째 인자 → 숫자 (여기선 product.price)
  • 두 번째 인자 → 최소 정수 자릿수 (0이면 그대로)
  • 세 번째 인자 → 정수 구분 방식 ('COMMA' → 3자리마다 콤마)
  • 네 번째 인자 → 최소 소수 자릿수
  • 다섯 번째 인자 → 소수 구분 방식 ('POINT' → 소수점)

예시

만약 product.price = 1234567 이라면

<td th:text="${#numbers.formatDecimal(product.price, 0, 'COMMA', 0, 'POINT')}"></td>

 

 

📌 참고: 금액에 원을 붙이고 싶다면

<td th:text="${#numbers.formatDecimal(product.price, 0, 'COMMA', 0, 'POINT')} + ' 원'"></td>

🌈 쇼핑몰 실습: 로그인 후 상품 추가·수정·삭제, 장바구니 구현

1. 세션(Session)이란?

  • 세션은 서버가 클라이언트를 구분하기 위한 정보 저장 공간
  • 로그인한 사용자 정보를 유지하거나, 특정 동작 후 돌아갈 목적지(destination)를 저장할 때 사용
  • HttpSession 객체를 통해 관리 (setAttribute, getAttribute, invalidate 등)

 

사용한 sql문

더보기
-------------------------------------------------------------------------------------
-- 회원 테이블
-------------------------------------------------------------------------------------
 DROP TABLE members CASCADE CONSTRAINTS;

create table members(
	id varchar2(10) primary key,
	name varchar2(30),
	password varchar2(30),
	gender varchar2(10),
	hobby varchar2(70),
	address varchar2(30),
	mpoint number default 0
) ;

insert into members(id,name,password,gender,hobby,address)
values('kim','김철수','1234','남자','게임','서울');

insert into members(id,name,password,gender,hobby,address)
values('hong','홍길동','1234','남자','공부','부산');

insert into members(id,name,password,gender,hobby,address)
values('park','권지현','1234','여자','마라톤,공부','제주');

commit ;

col id for a6
col password for a8
col name for a8
col gender for a6
col hobby for a15
col address for a10
col job for a6
select * from members;

-------------------------------------------------------------------------------------
-- 상품 테이블
-------------------------------------------------------------------------------------
drop sequence seqprod ;
create sequence seqprod start with 1 increment by 1 nocache ;

DROP TABLE products CASCADE CONSTRAINTS;

-- name : 상품명, company : 제조 회사, image : 상품 이미지
-- stock : 재고 수량, point : 적립 포인트, inputdate : 입고 일자, category : 카테고리
create table products(
	num 		int primary key,
	name 		varchar2(50) not null,
	company 	varchar2(50),
	image   	varchar2(30),
	stock		 int default 0,
	price   	int default 0,
	category   	varchar2(12),
	contents 	varchar2(300),
	point   	int default 0,
	inputdate 	date default sysdate
);

-------------------------------------------------------------------------------------
insert into products(num, name, company, image, stock, price, category, contents, point, inputdate)
values(seqprod.nextval, '소보루', '샤니', 'orange.jpg', 100, 1000, 'bread', '맛있어요', 10, sysdate );

insert into products(num, name, company, image, stock, price, category, contents, point, inputdate)
values(seqprod.nextval, '크림빵', '샤넬', 'blueberry.jpg', 50, 2000, 'bread', '맛있어요', 20, sysdate );

insert into products(num, name, company, image, stock, price, category, contents, point, inputdate)
values(seqprod.nextval, '콜라', '코카', 'melon.jpg', 30, 3000, 'beverage', '탁쏩니다', 30, sysdate );

insert into products(num, name, company, image, stock, price, category, contents, point, inputdate)
values(seqprod.nextval, '사이다', '칠성', 'kiui.jpg', 40, 4000, 'beverage', '탁쏩니다', 40, sysdate );

insert into products(num, name, company, image, stock, price, category, contents, point, inputdate)
values(seqprod.nextval, '환타', '코카', 'melon.jpg', 50, 5000, 'beverage', '탁쏩니다', 50, sysdate );

insert into products(num, name, company, image, stock, price, category, contents, point, inputdate)
values(seqprod.nextval, '치킨', '네네', 'pine.jpg', 50, 5000, 'chicken', '맛없어요', 60, sysdate);

commit ;
col name for a6
col company for a6
col category for a8
col stock for 999
col num for 99
col point for 99
col price for 9999
col contents for a10
col image for a14
select * from products;

-------------------------------------------------------------------------------------
-- 주문 (매출) 테이블
-------------------------------------------------------------------------------------
drop sequence seqoid ;
create sequence seqoid start with 1 increment by 1 nocache ;
DROP TABLE orders CASCADE CONSTRAINTS;

-- oid : 주문(송장) 번호, mid : 회원 번호, orderdate : 주문 일자
create table orders(
  oid number primary key,
  mid varchar2(10) references members(id) on delete set null,
  orderdate date default sysdate
);
select * from orders;



------------------------------------------------------------------------------------------------------
-- orderdetails : 주문 상세 테이블
-------------------------------------------------------------------------------------
drop sequence seqodid;
create sequence seqodid start with 1 increment by 1 nocache ;

-- oid : 주문번호,  pnum : 상품 번호,  qty : 주문 수량
drop table orderdetails purge ;

create table orderdetails(
  odid number primary key,
  oid number references orders(oid) on delete cascade,
  pnum number references products(num) on delete set null,
  qty number
);
select * from orderdetails;

 

 

home.html


화면 흐름

 

로그인X ➡️ 수정OR삭제OR 추가(삽입)시 ➡️ 로그인창 ➡️ 로그인 성공 ➡️ 수정OR삭제OR 추가폼

 

 

productList.html
memberLoginForm.html

 

수정폼


2. 로그인 로직 (MemberController)

화면 이미지 🔽

더보기
memberLoginForm.html
// home.html 로그인폼 이동
@GetMapping(value = "/login.mb")
public String login(){

    return "member/memberLoginForm";
}

// memberLoginForm.html에서 submit
    @PostMapping(value ="/login.mb")
    public String loginProc(MemberDto mDto, HttpSession session, HttpServletResponse response) throws IOException {
    // 1. 입력받은 아이디로 DB 조회
    MemberDto member = memberMapper.findById(mDto.getId());

    // 2. 회원이 없으면
    if(member == null){
        return "member/memberLoginForm"; // 로그인 폼 다시 이동
    }

    // 3. 회원이 있으면 비밀번호 확인
    if(member.getPassword().equals(mDto.getPassword())){
        // ✅ 로그인 성공 → 세션에 로그인 정보 저장
        session.setAttribute("loginInfo", member);

        // ✅ 이전에 가려던 페이지(destination)가 있으면 거기로 이동
        String destination = (String)session.getAttribute("destination");
        if(destination == null){
            return "home"; 
        } else {
            return destination; 
        }
    }else{
        return "member/memberLoginForm"; // 로그인 실패
    	}
   }

 

 

입력받은 id로 DB조회

MemberDto member = memberMapper.findById(mDto.getId()); 
존재하지 않는 id  ➡️ 로그인 화면 다시 이동
존재하는 id ➡️ 비밀번호 확인   

 

 

 로그인화면에서 아이디 비밀번호 입력 후 submit

⭕ 로그인 성공
session.setAttribute("loginInfo", member);  로그인 사용자 정보 저장
destination 세션값 확인 후 원래 가려던 페이지로 리다이렉트

 

❌로그인 실패
로그인 화면 다시 띄우기

 

 

 


3. 로그아웃 (MemberController)

화면 이미지 🔽

더보기

home.html 에서 로그아웃 버튼 클릭시 로그아웃

// 로그아웃
@GetMapping(value = "/logout.mb")
public String logout(HttpSession session){
    session.invalidate(); // 세션 강제종료
    return "redirect:/";
}

 

➡️invalidate() : 로그인 상태 해제, 세션 초기화


4. 상품 관리 시 세션 체크 (ProductsController)

화면 이미지 🔽

더보기

추가하기 버튼 클릭시 삽입폼 이동

 

(1) 삽입(insert) -> 삽입 폼으로 이동

@GetMapping("/pinsert.prd")
public String insertForm(HttpSession session, @ModelAttribute("pDto") ProductsDto pDto){
    if(session.getAttribute("loginInfo") == null){
        // 로그인 안 되어 있으면 → 로그인 페이지로 이동
        session.setAttribute("destination", "redirect:/pinsert.prd");
        return "redirect:/login.mb";
    }
    return "products/productInsert"; // 로그인 되어 있으면 삽입폼으로
}

 

 

(2) 수정(update) -> 수정 폼으로 이동

 // 수정폼으로 이동 (UpdateForm 이동)
    @GetMapping(value = "/pupdate.prd")
    public String updateForm(
            HttpSession session,
            @RequestParam("num") int num,  Model model,
            @RequestParam(value = "whatColumn", required = false) String whatColumn,
            @RequestParam(value = "keyword", required = false) String keyword,
            @RequestParam(value = "page",defaultValue = "1") int page
    ){


        if(session.getAttribute("loginInfo")==null){
            String encodeKeyword = keyword != null ? URLEncoder.encode(keyword, StandardCharsets.UTF_8) : "";
            session.setAttribute("destination", "redirect:/pupdate.prd?num=" + num
                    + "&page=" + page
                    + "&whatColumn=" + whatColumn
                    + "&keyword=" + encodeKeyword);
            return "redirect:/login.mb";
        }else{
            ProductsDto pDto = productsMapper.findByNum(num);
            model.addAttribute("keyword", keyword);
            model.addAttribute("whatColumn", whatColumn);
            model.addAttribute("page", page);
            model.addAttribute("pDto", pDto);
            return "products/productUpdate";
        }

 

수정(update) -> 수정 처리

// 수정처리 (Update)
    @PostMapping(value = "/pupdateProc.prd")
    public String updateProc(@RequestParam("num") int num, HttpSession session,
            @ModelAttribute("pDto") @Valid ProductsDto pDto,BindingResult br, Model model,
            @RequestParam(value = "whatColumn", required = false) String whatColumn,
            @RequestParam(value = "keyword", required = false) String keyword,
            @RequestParam(value = "page",defaultValue = "1") int page
    ) {
        if(session.getAttribute("loginInfo") == null){
            String encodeKeyword = keyword != null ? URLEncoder.encode(keyword, StandardCharsets.UTF_8) : "";
            session.setAttribute("destination", "redirect:/pupdate.prd?num=" + num
                    + "&page=" + page
                    + "&whatColumn=" + whatColumn
                    + "&keyword=" + encodeKeyword);
            return "redirect:/login.mb";
        }else{

            model.addAttribute("keyword", keyword);
            model.addAttribute("whatColumn", whatColumn);
            model.addAttribute("page", page);
            model.addAttribute("pDto", pDto);

            // 유효성 검정
            if(pDto.getFile() == null || pDto.getFile().isEmpty()) {
                br.rejectValue("file", "file.empty", "파일을 선택해주세요.");
            }

            if(br.hasErrors()) {
                return "products/productUpdate";
            }
            // 폴더 업로드
            MultipartFile file = pDto.getFile();
            String original = file.getOriginalFilename(); // 원본 파일명
            Path target = Paths.get(uploadDir,original); // 저장할 경로
            try {
                file.transferTo(target.toFile());        // 실제 파일 저장
            } catch (IOException e) {
                throw new RuntimeException(e);          // 예외 발생 시 런타임 에러
            }

            // DB에 저장할 파일명 세팅
            pDto.setImage(original);

            String resultPage = "";
            if (br.hasErrors()) {
                resultPage = "products/productUpdate";
            } else {
                productsMapper.updateProduct(pDto);
                String encodeKeyword = keyword != null ? URLEncoder.encode(keyword, StandardCharsets.UTF_8) : "";
                resultPage = "redirect:/plist.prd?page=" + page + "&whatColumn=" + whatColumn + "&keyword=" + encodeKeyword;
            }
            return resultPage;
        }
    }

 

 

 

(3) 삭제(delete) -> 삭제 요청시

 @GetMapping(value="/pdelete.prd")
    public String deleteProc(
            HttpSession session,
            @RequestParam("num") int num, Model model,
            @RequestParam(value = "whatColumn", required = false) String whatColumn,
            @RequestParam(value = "keyword", required = false) String keyword,
            @RequestParam(value = "page",defaultValue = "1") int page
    ) {

        if (session.getAttribute("loginInfo") == null) {
            String encodeKeyword = keyword != null ? URLEncoder.encode(keyword, StandardCharsets.UTF_8) : "";
            session.setAttribute("destination", "redirect:/pdelete.prd?num="+num
                    + "&page=" + page
                    + "&whatColumn=" + whatColumn
                    + "&keyword=" + encodeKeyword);
            return "redirect:/login.mb";
        } else {
            model.addAttribute("keyword", keyword);
            model.addAttribute("whatColumn", whatColumn);
            model.addAttribute("page", page);


            // ✅ 1. 삭제 전, 해당 상품의 이미지 파일명 조회 (이미지 파일 삭제)
            ProductsDto pDto = productsMapper.findByNum(num);
            if (pDto != null && pDto.getImage() != null) {
                Path filePath = Paths.get(uploadDir, pDto.getImage());
                File file = filePath.toFile(); // Path → File 객체 변환
                    file.delete();         // 2) 파일삭제 방법2
                    System.out.println("이미지 삭제 성공 :" + file);
            }
            // ✅ 2. DB 레코드 삭제
            productsMapper.deleteProduct(num);

            // ✅ 3. 페이징 처리
            int limit = 5;
            int offset = (page - 1) * limit;

            Map<String, Object> params = new HashMap<String, Object>();
            params.put("whatColumn", whatColumn);
            params.put("keyword", keyword);
            params.put("offset", offset);
            params.put("limit", limit);

            int totalCount = productsMapper.getCount(params);
            if (totalCount % limit == 0) {
                page = page - 1;
            }
            String encodeKeyword = keyword != null ? URLEncoder.encode(keyword, StandardCharsets.UTF_8) : "";
            return "redirect:/plist.prd?page=" + page + "&whatColumn=" + whatColumn + "&keyword=" + encodeKeyword;
        }
    }
1. 로그인시

session.setAttribute("loginInfo", member);  
👉 로그인에 성공하면 loginInfo라는 이름으로 회원정보(MemberDto)를 세션에 저장.
 
2. 로그인 확인 : 세션에서 loginInfo를 꺼내와서(null인지 여부 확인) → 로그인 안 했는지 여부 판별.

if(session.getAttribute("loginInfo") == null) { ... }
 
3. 미로그인 → 로그인 페이지로 강제 이동

return "redirect:/login.mb";
 
4. 로그인 되어있으면 원래 작업 진행

 

 

5. 흐름 요약 (Session + Destination 활용)

✅ 로그인 X 상태

  1. 상품 삽입 / 수정 / 삭제 시도
  2. → 로그인 창 이동 (destination 에 원래 주소 저장)
  3. 로그인 성공
  4. → destination 값 확인 → 원래 시도한 페이지로 이동

✅ 로그인 O 상태

  • 바로 삽입 / 수정 / 삭제 가능

6. 화면 흐름 (간단 다이어그램)

[로그인 X] 
   ↓ (삽입/수정/삭제 버튼 클릭)
[로그인 페이지] ← destination 저장
   ↓ (로그인 성공)
[원래 목적지 폼]

[로그인 O]
   ↓
[바로 삽입/수정/삭제 폼]

7. 정리

  • 세션(HttpSession)은 로그인 유지 + 권한 제어 + 이전 페이지 기억 에 활용된다.
  • loginInfo : 로그인 사용자 정보 저장
  • destination : 로그인 후 되돌아갈 주소 저장
  • invalidate() : 로그아웃 시 세션 삭제

🌈8. 장바구니 구현 (MallController / mallList.html)

 

상품 상세에서 주문 버튼 주문버튼 클릭시 장바구니 이동

productContent.prd ➡️ mall.list 이동

 

 

(1) MyCartDto

package com.example.Ex02.dto;

import java.util.HashMap;
import java.util.Map;

public class MyCartList {
    private Map<Integer, Integer> orderlists = null;

    public MyCartList(){
        orderlists = new HashMap<Integer, Integer>(); // key, value
        // 3번 상품, 2개 주문
        // 1번 상품, 5개 주문

    }

    // 장바구니 있는 상품을 더 추가할 경우
    public void addOrder(int pnum, int oqty){
        // orderlists : Map<Integer, Integer>
        // key = 상품번호(pnum), value = 주문수량(qty)

        if(orderlists.containsKey(pnum)){ // ✅ 이미 장바구니에 같은 상품이 있는 경우
            Integer qty = orderlists.put(pnum, oqty);
            int addQty = qty + oqty;
            orderlists.put(pnum,addQty); // 기존 수량 + 새 수량으로 덮어쓰기
        }else{// ✅새상품이면
            orderlists.put(pnum,oqty);  // 그대로 추가
        }
    }


   public Map<Integer,Integer> getAllOrderlists(){

        return orderlists;
    }


}

 

 

(2) mallController

1. 장바구니 구현 컨트롤러에 로그인 세션 가져오기 (로그인 여부확인)

2. 장바구니 세션 생성

// productContent.prd => 주문버튼 클릭시 장바구니 이동
    @PostMapping(value = "/add.mall")
    public String add(ProductsDto product, HttpSession session,
                      @RequestParam(value = "whatColumn", required = false) String whatColumn,
                      @RequestParam(value = "keyword", required = false) String keyword,
                      @RequestParam(value = "page",defaultValue = "1") int page
                      ){
        System.out.println("product.getNum()" + product.getNum());
        System.out.println("product.getOrerqty()" + product.getOrderqty());

        // memberController에서 설정한 loginInfo를 get으로 불러와서 사용
        // 1. 로그인 여부 확인 (세션에 "loginInfo"가 없으면 로그인 안된 상태)
        if(session.getAttribute("loginInfo") == null){
            String encodeKeyword = keyword != null ? URLEncoder.encode(keyword, StandardCharsets.UTF_8) : "";
            session.setAttribute("destination", "redirect:/pcontent.prd?" +
                    "num="+product.getNum()
                    +"&page="+page
                    +"&whatColumn="+whatColumn
                    +"&keyword=" +encodeKeyword);
            return "redirect:/login.mb";
        }else{
            // 2. 로그인 된 경우 → 장바구니 처리
            // 장바구니 세션 설정 : mycart
            MyCartList mycart = (MyCartList) session.getAttribute("mycart");
            System.out.println("mycart1 : " + mycart);
            if(mycart == null){ // 3. 장바구니가 없다면  == > 장바구니 생성
                mycart = new MyCartList(); // "장바구니 객체" 생성
            }
            System.out.println("mycart2 : " + mycart);

            // 4. 장바구니에 상품 추가
            mycart.addOrder(product.getNum(), product.getOrderqty());

            // 5. 장바구니를 계속 끌고다닐수 있도록 세션 설정
            session.setAttribute("mycart", mycart);
            return "redirect:/list.mall";
        }
    }

 

(3) mallList.html

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h3 style="text-align:center;">mall/mallList</h3>
<div th:insert ="~{fragment :: header}"></div>
<table border="1" width="400" style="text-align:center; border-collapse:collapse;">
    <!-- 주문자 정보 -->
    <tr>
        <th colspan="5" class="order-info">
            주문자 :
            <span th:text="${session.loginInfo.name}"></span>
            (<span th:text="${session.loginInfo.id}"></span>)
        </th>
    </tr>

    <!-- 헤더 -->
    <tr>
        <th>상품번호</th>
        <th>상품명</th>
        <th>주문수량</th>
        <th>단가</th>
        <th>금액</th>
    </tr>

    <!-- 상품 목록 반복 -->
    <tr th:each="product : ${shopLists}">
        <td th:text="${product.pnum}"></td>
        <td th:text="${product.pname}"></td>
        <td th:text="${product.qty}"></td>
        <td th:text="${#numbers.formatDecimal(product.price,0,'COMMA',0,'POINT')}"></td>
        <td th:text="${#numbers.formatDecimal(product.amount,0,'COMMA',0,'POINT')}"></td>
    </tr>

    <!-- 총 합계 -->
    <tr class="total-row">
        <td colspan="2">총 합계</td>
        <td colspan="3"
            th:text="${#numbers.formatDecimal(totalAmount, 0, 'COMMA', 0, 'POINT')}"></td>
    </tr>
</table>

<!-- 액션 버튼 -->
<div class="actions">
    <a th:href="@{/calculate.mall}">결제하기</a> |
    <a th:href="@{/plist.prd}">추가 주문</a>
</div>

</body>
</html>

🌈9. 주문내역 구현 (MallController)

mallList.html에서 결제하기 버튼 클릭시 이동

💳 결제 완료(calculate.mall) 시

  1. 이미 장바구니의 데이터를 기반으로
    orders (주문 테이블)
    orderdetails (주문 상세 테이블)
     영구 저장(DB insert) 
    → 즉, 장바구니의 정보는 “주문으로 승격됨”.
  2. 결제가 끝났는데 장바구니를 그대로 두면,
    • 사용자가 “결제하기” 버튼을 다시 누르면 중복 주문 발생 위험 ⚠️
    • 결제 완료 후에도 장바구니에 남아 있으면 UX 혼란
      (“이거 아직 결제 안 된 건가?”처럼 보임)
  3. 그래서 결제 완료 후엔 즉시:
session.removeAttribute("mycart");

→ 세션에서 mycart 객체를 삭제하여 장바구니를 완전히 비움.
→ 즉, 새 쇼핑을 시작할 수 있는 “초기 상태”로 되돌림

 

로그인 후 나의 주문내역 보기

 

orderMapper 전체코드

더보기
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<!--orders.xml-->
<mapper namespace="com.example.Ex02.mapper.OrderMapper">
        <insert id="insertOrder" parameterType="com.example.Ex02.dto.OrderDto">
            insert into orders (oid, mid, orderdate)
            values (seqoid.nextval, #{mid}, sysdate)
        </insert>

        <!--가장 큰 oid값 -->
        <select id="getMaxOid" resultType="int">
            select max(oid)
            from orders
        </select>

        <!--주문자 주문목록 찾기-->
        <select id="orderMall">
            select * from orders
            where mid = #{id}
            order by oid desc
        </select>

        <!--주문 상세보기-->
        <select id="showDetail">
            select num as pnum, name as pname, qty, price, qty*price as amount
            from (orders o inner join orderdetails od
            on o.oid = od.oid) inner join products
            on pnum = num and o.oid = #{oid}
        </select>

</mapper>

 

(1) MallController

orderMapper.insertOrder() : 주문 기분정보  DB저장

orderMapper.getMaxOid() : 방금 넣은 주문의 PK가져오기

orderDetailsMapper.insertOrderDetails() : 상세보기dto에 값넣기

productsMapper.updateStock(pDto) : 주문시 장품 재고 차감

memberMapper.addPoint(member) : 주문시 포인트 적립

member은 사용자로그인 정보 (세션)으로 가져옴

 

orderDatailMapper

더보기
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<!--orderdetails.xml-->
<mapper namespace="com.example.Ex02.mapper.OrderDetailsMapper">
    <insert id="insertOrderDetails">
        insert into orderdetails
        values(seqodid.nextval, #{oid}, #{pnum}, #{qty})
    </insert>

</mapper>

 

(장바구니 → 주문 테이블 → 주문상세 테이블 → 포인트 적립 → 장바구니 비우기)

// 💳 주문내역 처리 (mallList.html에서 "결제하기" 클릭 시 실행)
@GetMapping(value = "/calculate.mall")
public String calculate(HttpSession session, Model model) {

    // ✅ 1️⃣ 세션에서 장바구니 정보 가져오기
    // (장바구니는 MyCartList 객체로 세션에 저장되어 있음)
    MyCartList mycart = (MyCartList) session.getAttribute("mycart");

    // 장바구니가 비어있지 않은 경우에만 주문 처리
    if (mycart != null) {

        // ✅ 2️⃣ 장바구니의 전체 상품목록 가져오기
        // 예시: {4=3, 6=2} → 상품번호 4는 3개, 상품번호 6은 2개
        Map<Integer, Integer> maplists = mycart.getAllOrderlists();

        // ✅ 3️⃣ 로그인한 사용자 정보 가져오기 (주문자 ID 저장용)
        MemberDto member = (MemberDto) session.getAttribute("loginInfo");

        // ✅ 4️⃣ 주문 기본정보 저장 (orders 테이블)
        // 주문자 ID(mid)만 먼저 넣고, 나머지는 DB에서 자동 처리 (oid는 시퀀스로)
        OrderDto orderDto = new OrderDto();
        orderDto.setMid(member.getId()); // ex) "kim"
        orderMapper.insertOrder(orderDto); // → orders 테이블에 삽입

        // ✅ 5️⃣ 방금 저장한 주문의 고유번호(oid) 가져오기
        // 최신 주문의 PK(oid)를 구해서 주문상세(orderdetails)에 연결하기 위함
        Set<Integer> keylist = maplists.keySet(); // ex) [4, 6, 10]
        int maxOid = orderMapper.getMaxOid();     // ex) 23 (이번 주문번호)
        System.out.println("maxOid: " + maxOid);

        // ✅ 6️⃣ 장바구니의 모든 상품을 반복하면서 주문상세(orderdetails)에 저장
        for (Integer pnum : keylist) {
            OrderDetailDto odDto = new OrderDetailDto();

            // 해당 상품의 수량 가져오기
            int qty = maplists.get(pnum);
            System.out.println("qty: " + qty);

            // 주문상세 데이터 설정
            odDto.setOid(maxOid); // 주문번호(FK)
            odDto.setPnum(pnum);  // 상품번호
            odDto.setQty(qty);    // 수량

            // DB에 주문상세 레코드 추가
            orderDetailsMapper.insertOrderDetails(odDto);

            // ✅ 7️⃣ 상품 재고 차감 (상품테이블 업데이트)
            ProductsDto pDto = new ProductsDto();
            pDto.setNum(pnum);
            pDto.setStock(qty);
            productsMapper.updateStock(pDto); // → stock = stock - qty
        } // for

        // ✅ 8️⃣ 주문 완료 시 포인트 적립 (예: 100점)
        int point = 100;
        member.setId(member.getId());
        member.setMpoint(point);
        memberMapper.addPoint(member);

    } // if(mycart != null)

    // ✅ 9️⃣ 결제 완료 후 장바구니 세션 비우기
    // (중복 결제 방지 및 새로운 쇼핑을 위한 초기화)
    session.removeAttribute("mycart");

    // ✅ 🔟 결제 완료 후 상품 리스트 페이지로 이동
    return "redirect:/plist.prd";
}

 


🌈10. 나의 주문내역보기 구현 (MallController)

home에서 주문내역 보기
로그인을 안했을경우 로그인 창
로그인 후 나의 주문내역 보기
주문내역 상세보기 화면

 

 

 

 

(1) 나의 주문내역 보기

orderMapper.orderMall() : mapper에서 사용자 ID로 주문목록 조회

 

// 나의 주문내역 보기
@GetMapping(value = "/order.mall")
public String order(HttpSession session, Model model){

    // ✅ 1️⃣ 로그인 정보 확인
    // 세션에 저장된 로그인 사용자 정보(loginInfo) 가져오기
    MemberDto loginInfo = (MemberDto)session.getAttribute("loginInfo");

    // ✅ 2️⃣ 로그인하지 않은 경우 → 로그인 페이지로 이동
    if(loginInfo == null){
        session.setAttribute("destination", "redirect:/order.mall");
        return "redirect:/login.mb";
    }else{
        // ✅ 3️⃣ 로그인한 경우 → 해당 사용자의 주문 내역 조회
        // orderMapper.orderMall() : mapper에서 사용자 ID로 주문 목록 조회
       List<OrderDto>lists = orderMapper.orderMall(loginInfo);
       model.addAttribute("lists", lists);
    }
 return "mall/shopList";
}

 

(2) 나의 주문내역 상세보기

orderMapper.showDeatail() 

 

// 🧾 나의 주문내역 상세보기
@GetMapping(value = "/detailview.mall")
public String detailview(@RequestParam("oid") int oid, HttpSession session, Model model) {

    // ✅ 1️⃣ 로그인 여부 확인
    // 세션에서 로그인한 사용자 정보(loginInfo) 가져오기
    MemberDto loginInfo = (MemberDto) session.getAttribute("loginInfo");

    // ✅ 2️⃣ 로그인하지 않은 경우 → 로그인 페이지로 이동
    if (loginInfo == null) {
        // 로그인 성공 후 돌아올 페이지를 저장
        session.setAttribute("destination", "redirect/plist.prd");

        // 로그인 페이지로 리다이렉트
        return "redirect:/login.mb";
    } 
    // ✅ 3️⃣ 로그인한 경우 → 주문 상세내역 조회
    else {
        // 주문 번호(oid)를 모델에 담기 (View에서 사용 가능)
        model.addAttribute("oid", oid);

        // ✅ 4️⃣ 매퍼 호출: 주문상세(orderdetails)와 상품(products) 테이블을 조인하여 상세정보 조회
        List<ShoppingInfo> lists = orderMapper.showDetail(oid);
        System.out.println("lists.size() : " + lists.size());
        model.addAttribute("lists", lists);

        // 주문상세 결과 페이지(shopResult.html)로 이동
        return "mall/shopResult";
    }

    /* 
     📌 Mapper 설명
     orderdetails 테이블의 pnum(상품번호)과 
     products 테이블의 num(상품번호)을 JOIN하여 
     주문 상세 정보(상품명, 수량, 가격 등)를 함께 조회.
    */
}
“로그인한 사용자가 특정 주문번호(oid)의 상세내역을 조회하는 메서드로,
주문상세(orderdetails)와 상품정보(products)를 조인하여
하나의 리스트로 View에 전달한다.”

📂 Spring 파일 관리 주요 기능/메서드 정리

 

1. 업로드 (Upload)

 

👉 MultipartFile 메서드 기반

  • getOriginalFilename() : 원본 파일명
  • getSize() : 파일 크기
  • getContentType() : MIME 타입 (예: image/png, application/pdf)
  • isEmpty() : 파일 비어있는지 여부
  • transferTo(File dest) : 실제 저장

2. 조회 (Read)

👉 저장된 파일을 다시 가져오거나 다운로드할 때 사용

(1) Files.exists(Path)

  • 경로에 파일이 존재하는지 확인
if (Files.exists(Paths.get(uploadDir, filename))) {
    System.out.println("파일 있음");
}

2) Resource resource = new UrlResource(path.toUri())

  • 스프링에서 파일 다운로드 시 ResponseEntity<Resource>로 반환 가능
Path file = Paths.get(uploadDir).resolve(filename);
Resource resource = new UrlResource(file.toUri());

 

 


3. 수정 (Update)

  • 보통 수정 = 교체(삭제 후 재업로드) 로 처리

(1) 기존 파일 삭제

Files.deleteIfExists(Paths.get(uploadDir, oldFilename));

 

(2) 새 파일 업로드

MultipartFile newFile = dto.getFile();
newFile.transferTo(Paths.get(uploadDir, newFile.getOriginalFilename()));

4. 삭제 (Delete)

(1) Files.delete(Path)

  • 파일 삭제. 파일 없으면 예외 발생
  • 👉 무조건 존재하는 파일만 지우고 싶을 때.
Path path = Paths.get(uploadDir, filename);
try {
    Files.delete(path); // 파일 없으면 NoSuchFileException
    System.out.println("삭제 성공");
} catch (IOException e) {
    e.printStackTrace(); // 파일 없거나 권한 문제 모두 예외 처리
}

 

(2) Files.deleteIfExists(Path)

  • 파일 없으면 false 반환, 안전하게 삭제
  • 👉 deleteIfExists는 존재하지 않아도 예외가 안 터지니까 실무에서 가장 많이 씀.
Path path = Paths.get(uploadDir, filename);
try {
    Files.deleteIfExists(path); // 파일이 없으면 false 반환
    System.out.println("삭제 성공 또는 파일 없음");
} catch (IOException e) {
    e.printStackTrace(); // 접근 권한 문제 등만 잡으면 됨
}

 

(3) file.delete();

👉 예외 안 터지고 true/false만 확인할 수 있음.

 Path filePath = Paths.get(uploadDir, pDto.getImage());
 File file = filePath.toFile();
 file.delete();

5. 파일 정보 조회 (MetaData)

(1) Files.size(Path)

  • 파일 크기 가져오기
long size = Files.size(Paths.get(uploadDir, filename));

 

(2) Files.probeContentType(Path)

  • 파일 MIME 타입 확인
String type = Files.probeContentType(Paths.get(uploadDir, filename));

 

3) Files.getLastModifiedTime(Path)

  • 마지막 수정 시간 확인
FileTime time = Files.getLastModifiedTime(Paths.get(uploadDir, filename));

📌 CRUD 흐름 예시

// 파일 업로드
MultipartFile file = dto.getFile();
String filename = file.getOriginalFilename();
Path target = Paths.get(uploadDir, filename);
file.transferTo(target.toFile());

// 파일 조회 (다운로드 준비)
Resource resource = new UrlResource(target.toUri());

// 파일 수정 (기존 삭제 후 업로드)
Files.deleteIfExists(Paths.get(uploadDir, oldFilename));
file.transferTo(Paths.get(uploadDir, newFilename).toFile());

// 파일 삭제
Path file = Paths.get(uploadDir,pDto.getImage())
Files.deleteIfExists(file); // 방법1)
Files.delete(file);    //방법2)

 

예시 실습코드 

2025.09.30 - [▶ Spring] - 11. Spring 쇼핑몰_파일첨부와_이미지출력

+ Recent posts