<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>jhwest.log</title>
        <link>https://velog.io/</link>
        <description></description>
        <lastBuildDate>Wed, 20 May 2026 07:41:20 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>jhwest.log</title>
            <url>https://velog.velcdn.com/images/jhwest-dev/profile/4e60fe00-4e0a-451f-8d52-95a10d5a7e6f/image.jpg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. jhwest.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/jhwest-dev" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[4편 - [Spring Boot] 피자 가게 - 회원 관리 (회원가입 / 로그인)]]></title>
            <link>https://velog.io/@jhwest-dev/Spring-Boot-%ED%9A%8C%EC%9B%90-%EA%B4%80%EB%A6%AC-%EA%B0%80%EC%9E%85-%EB%A1%9C%EA%B7%B8%EC%9D%B8</link>
            <guid>https://velog.io/@jhwest-dev/Spring-Boot-%ED%9A%8C%EC%9B%90-%EA%B4%80%EB%A6%AC-%EA%B0%80%EC%9E%85-%EB%A1%9C%EA%B7%B8%EC%9D%B8</guid>
            <pubDate>Wed, 20 May 2026 07:41:20 GMT</pubDate>
            <description><![CDATA[<hr>
<h2 id="이-글에서-다룰-내용">이 글에서 다룰 내용</h2>
<ol>
<li>프로젝트 구조 설명</li>
<li>회원가입 백엔드<ul>
<li>DB 테이블 생성</li>
<li>MemberVO</li>
<li>DTO 생성 (Request / Response)</li>
<li>MemberMapper</li>
<li>MemberService</li>
<li>MemberController</li>
</ul>
</li>
<li>회원가입 프론트</li>
<li>로그인 백엔드<ul>
<li>MemberService</li>
<li>MemberController</li>
</ul>
</li>
<li>로그인 프론트</li>
<li>공통 기능 (common.js)</li>
<li>동작 확인</li>
</ol>
<hr>
<h2 id="1-프로젝트-구조-설명">1. 프로젝트 구조 설명</h2>
<h3 id="백엔드">백엔드</h3>
<pre><code>pizza-shop/
├── src/main/java/
│   └── com.pizzashop/
│       ├── controller/
│       │   ├── MenuController.java
│       │   └── MemberController.java
│       ├── dto/
│       │   ├── request/
│       │   │   ├── MemberRegisterRequest.java
│       │   │   └── MemberLoginRequest.java
│       │   └── response/
│       │       ├── ApiResponse.java
│       │       ├── MemberLoginResponse.java
│       ├── mapper/
│       │   ├── MenuMapper.java
│       │   └── MemberMapper.java
│       ├── service/
│       │   ├── MenuService.java
│       │   └── MemberService.java
│       └── vo/
│           ├── MenuVO.java
│           └── MemberVO.java</code></pre><table>
<thead>
<tr>
<th>패키지</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><code>controller</code></td>
<td>클라이언트 요청을 받아 응답을 반환</td>
</tr>
<tr>
<td><code>dto/request</code></td>
<td>클라이언트에서 받는 요청 데이터</td>
</tr>
<tr>
<td><code>dto/response</code></td>
<td>클라이언트에게 보내는 응답 데이터</td>
</tr>
<tr>
<td><code>mapper</code></td>
<td>MyBatis를 통해 DB와 직접 통신</td>
</tr>
<tr>
<td><code>service</code></td>
<td>비즈니스 로직 처리 (중복 확인, 비밀번호 검증 등)</td>
</tr>
<tr>
<td><code>vo</code></td>
<td>DB 테이블과 1:1 매핑되는 데이터 객체</td>
</tr>
</tbody></table>
<blockquote>
<p>Controller → Service → Mapper → DB 순서로 요청이 흘러간다.</p>
</blockquote>
<h3 id="프론트엔드">프론트엔드</h3>
<pre><code>PIZZA-SHOP/
├── css/
│   ├── style.css
│   └── auth.css
├── js/
│   ├── common.js
│   ├── menu.js
│   └── auth.js
├── index.html
├── login.html
└── register.html</code></pre><table>
<thead>
<tr>
<th>파일</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><code>auth.css</code></td>
<td>로그인 / 회원가입 페이지 전용 스타일</td>
</tr>
<tr>
<td><code>common.js</code></td>
<td>공통 기능 (updateNav, logout)</td>
</tr>
<tr>
<td><code>auth.js</code></td>
<td>로그인 / 회원가입 API 연동</td>
</tr>
<tr>
<td><code>login.html</code></td>
<td>로그인 페이지</td>
</tr>
<tr>
<td><code>register.html</code></td>
<td>회원가입 페이지</td>
</tr>
</tbody></table>
<hr>
<h2 id="2-회원가입-백엔드">2. 회원가입 백엔드</h2>
<p>요청 흐름은 다음과 같다.</p>
<pre><code>클라이언트 → Controller → Service → Mapper → DB</code></pre><h3 id="db-테이블-생성">DB 테이블 생성</h3>
<p>회원 정보를 저장할 <code>members</code> 테이블을 먼저 생성한다.</p>
<pre><code class="language-sql">CREATE TABLE IF NOT EXISTS members (
    id INT AUTO_INCREMENT PRIMARY KEY,
    username VARCHAR(50) UNIQUE NOT NULL,
    password VARCHAR(100) NOT NULL,
    name VARCHAR(50) NOT NULL,
    role VARCHAR(20) DEFAULT &#39;USER&#39;,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);</code></pre>
<table>
<thead>
<tr>
<th>컬럼</th>
<th>타입</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><code>id</code></td>
<td>INT</td>
<td>고유번호 (자동 증가)</td>
</tr>
<tr>
<td><code>username</code></td>
<td>VARCHAR(50)</td>
<td>로그인 아이디 (중복 불가)</td>
</tr>
<tr>
<td><code>password</code></td>
<td>VARCHAR(100)</td>
<td>비밀번호 (실무에서는 암호화 필요)</td>
</tr>
<tr>
<td><code>name</code></td>
<td>VARCHAR(50)</td>
<td>닉네임</td>
</tr>
<tr>
<td><code>role</code></td>
<td>VARCHAR(20)</td>
<td>권한 (USER / ADMIN, 기본값 USER)</td>
</tr>
<tr>
<td><code>created_at</code></td>
<td>TIMESTAMP</td>
<td>가입일 (자동 저장)</td>
</tr>
</tbody></table>
<hr>
<h3 id="membervo">MemberVO</h3>
<p>DB 테이블과 1:1로 매핑되는 데이터 객체다.</p>
<pre><code class="language-java">@Data
@NoArgsConstructor
@AllArgsConstructor
public class MemberVO {
    private int id;           // 고유번호
    private String username;  // 로그인 아이디
    private String password;  // 비밀번호 (실무에서는 암호화 필요)
    private String name;      // 닉네임
    private String role;      // 권한 (USER / ADMIN)
    private String createdAt; // 가입일
}</code></pre>
<table>
<thead>
<tr>
<th>어노테이션</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><code>@Data</code></td>
<td>getter, setter, toString 자동 생성</td>
</tr>
<tr>
<td><code>@NoArgsConstructor</code></td>
<td>기본 생성자 자동 생성</td>
</tr>
<tr>
<td><code>@AllArgsConstructor</code></td>
<td>전체 필드 생성자 자동 생성</td>
</tr>
</tbody></table>
<hr>
<h3 id="dto-생성">DTO 생성</h3>
<p>클라이언트와 주고받는 데이터를 명확하게 분리하기 위해 DTO를 사용한다.</p>
<blockquote>
<p>VO를 그대로 사용하면 클라이언트가 <code>role</code> 같은 민감한 필드를 임의로 넘길 수 있어 보안에 취약하다. DTO를 사용하면 필요한 필드만 주고받을 수 있다.</p>
</blockquote>
<p><strong>MemberRegisterRequest</strong> - 회원가입 요청 데이터</p>
<pre><code class="language-java">@Data
@NoArgsConstructor
@AllArgsConstructor
public class MemberRegisterRequest {
    private String username;
    private String password;
    private String name;
}</code></pre>
<p><strong>MemberLoginRequest</strong> - 로그인 요청 데이터</p>
<pre><code class="language-java">@Data
@NoArgsConstructor
@AllArgsConstructor
public class MemberLoginRequest {
    private String username;
    private String password;
}</code></pre>
<p><strong>ApiResponse</strong> - 공통 응답 데이터</p>
<pre><code class="language-java">@Data
@NoArgsConstructor
@AllArgsConstructor
public class ApiResponse {
    private String result;  // &quot;ok&quot; 또는 &quot;fail&quot;
    private String message; // 성공 또는 실패 메시지
}</code></pre>
<p><strong>MemberLoginResponse</strong> - 로그인 응답 데이터</p>
<pre><code class="language-java">@Data
@NoArgsConstructor
@AllArgsConstructor
public class MemberLoginResponse {
    private String result;   // 실행 결과
    private String name;     // 사용자 이름
    private String role;     // 사용자 역할 (USER / ADMIN)
    private String username; // 사용자 아이디
}</code></pre>
<blockquote>
<p>로그인 응답에 <code>password</code>는 절대 포함하면 안 된다.</p>
</blockquote>
<hr>
<h3 id="membermapper">MemberMapper</h3>
<pre><code class="language-java">@Mapper
public interface MemberMapper {

    // 아이디 중복 체크 / 로그인 시 회원 조회
    @Select(&quot;SELECT * FROM members WHERE username = #{username}&quot;)
    MemberVO findByUsername(String username);

    // 회원가입 INSERT
    // id, role, created_at은 DB에서 자동 생성되므로 쿼리에서 제외
    @Insert(&quot;INSERT INTO members(username, password, name) &quot;
            + &quot;VALUES (#{username}, #{password}, #{name})&quot;)
    @Options(useGeneratedKeys = true, keyProperty = &quot;id&quot;)
    int insert(MemberVO member);
}</code></pre>
<table>
<thead>
<tr>
<th>메서드</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><code>findByUsername</code></td>
<td>아이디로 회원 조회 (중복 체크 / 로그인에 사용)</td>
</tr>
<tr>
<td><code>insert</code></td>
<td>회원 정보 DB에 저장</td>
</tr>
</tbody></table>
<blockquote>
<p><code>@Options(useGeneratedKeys = true, keyProperty = &quot;id&quot;)</code> 는 INSERT 후 자동 생성된 id 값을 MemberVO.id에 다시 넣어준다.</p>
</blockquote>
<hr>
<h3 id="memberservice---회원가입">MemberService - 회원가입</h3>
<pre><code class="language-java">@Service
public class MemberService {

    @Autowired
    private MemberMapper memberMapper;

    // 1. 같은 아이디가 DB에 존재하는지 조회
    // 2. 있으면 false 반환 -&gt; Controller에서 &quot;중복 아이디&quot; 응답
    // 3. 없으면 INSERT 실행 -&gt; 성공 시 true 반환
    public boolean register(MemberRegisterRequest request) {
        MemberVO existing = memberMapper.findByUsername(request.getUsername());
        if (existing != null) {
            return false; // 중복 아이디 -&gt; 가입 거절
        }

        // Request -&gt; VO 변환
        MemberVO member = new MemberVO();
        member.setUsername(request.getUsername());
        member.setPassword(request.getPassword());
        member.setName(request.getName());

        return memberMapper.insert(member) &gt; 0;
    }
}</code></pre>
<blockquote>
<p><code>role</code>과 <code>created_at</code>은 DB에서 자동으로 설정되므로 따로 넣지 않아도 된다.</p>
</blockquote>
<hr>
<h3 id="membercontroller---회원가입">MemberController - 회원가입</h3>
<pre><code class="language-java">@RestController
@RequestMapping(&quot;/api/member&quot;)
@CrossOrigin(origins = &quot;*&quot;)
public class MemberController {

    @Autowired
    private MemberService memberService;

    // POST /api/member/register
    // 요청 Body : {&quot;username&quot;: &quot;hong&quot;, &quot;password&quot;: &quot;1234&quot;, &quot;name&quot;: &quot;홍길동&quot;}
    // 응답 : {&quot;result&quot;: &quot;ok&quot;, &quot;message&quot;: &quot;회원가입 완료&quot;}
    @PostMapping(&quot;/register&quot;)
    public ApiResponse register(@RequestBody MemberRegisterRequest request) {
        boolean ok = memberService.register(request);
        if (ok) {
            return new ApiResponse(&quot;ok&quot;, &quot;회원가입 완료&quot;);
        } else {
            return new ApiResponse(&quot;fail&quot;, &quot;이미 사용 중인 아이디입니다.&quot;);
        }
    }
}</code></pre>
<table>
<thead>
<tr>
<th>어노테이션</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><code>@RestController</code></td>
<td>Controller + ResponseBody, JSON 자동 반환</td>
</tr>
<tr>
<td><code>@RequestMapping</code></td>
<td>공통 URL prefix <code>/api/member</code> 설정</td>
</tr>
<tr>
<td><code>@CrossOrigin</code></td>
<td>프론트에서 API 호출 허용 (CORS 설정)</td>
</tr>
<tr>
<td><code>@PostMapping</code></td>
<td>POST 요청 처리</td>
</tr>
<tr>
<td><code>@RequestBody</code></td>
<td>JSON 요청 Body를 DTO로 변환</td>
</tr>
</tbody></table>
<hr>
<h2 id="3-회원가입-프론트">3. 회원가입 프론트</h2>
<h3 id="registerhtml">register.html</h3>
<pre><code class="language-html">&lt;div class=&quot;auth-container&quot;&gt;
    &lt;div class=&quot;auth-box&quot;&gt;
        &lt;h2&gt;회원가입&lt;/h2&gt;
        &lt;div class=&quot;form-group&quot;&gt;
            &lt;label&gt;아이디&lt;/label&gt;
            &lt;input type=&quot;text&quot; id=&quot;regId&quot; placeholder=&quot;아이디를 입력하세요&quot; /&gt;
        &lt;/div&gt;
        &lt;div class=&quot;form-group&quot;&gt;
            &lt;label&gt;비밀번호&lt;/label&gt;
            &lt;input type=&quot;password&quot; id=&quot;regPw&quot; placeholder=&quot;비밀번호를 입력하세요&quot; /&gt;
        &lt;/div&gt;
        &lt;div class=&quot;form-group&quot;&gt;
            &lt;label&gt;이름&lt;/label&gt;
            &lt;input type=&quot;text&quot; id=&quot;regName&quot; placeholder=&quot;이름을 입력하세요&quot; /&gt;
        &lt;/div&gt;
        &lt;button class=&quot;btn-submit&quot; onclick=&quot;register()&quot;&gt;회원가입&lt;/button&gt;
        &lt;p class=&quot;auth-link&quot;&gt;
            이미 계정이 있으신가요? &lt;a href=&quot;login.html&quot;&gt;로그인&lt;/a&gt;
        &lt;/p&gt;
    &lt;/div&gt;
&lt;/div&gt;</code></pre>
<h3 id="authjs---회원가입">auth.js - 회원가입</h3>
<pre><code class="language-javascript">const AUTH_API = &quot;http://localhost:8080/api/member&quot;;

function register() {
    const id = document.getElementById(&quot;regId&quot;).value.trim();
    const pw = document.getElementById(&quot;regPw&quot;).value.trim();
    const name = document.getElementById(&quot;regName&quot;).value.trim();

    if (!id || !pw || !name) {
        alert(&quot;모든 항목을 입력해 주세요.&quot;);
        return;
    }

    fetch(`${AUTH_API}/register`, {
        method: &quot;POST&quot;,
        headers: { &quot;Content-Type&quot;: &quot;application/json&quot; },
        body: JSON.stringify({ username: id, password: pw, name: name }),
    })
        .then((res) =&gt; res.json())
        .then((data) =&gt; {
            if (data.result === &quot;ok&quot;) {
                alert(&quot;가입 완료! 로그인 페이지로 이동합니다.&quot;);
                setTimeout(() =&gt; {
                    location.href = &quot;login.html&quot;;
                }, 1500);
            } else {
                alert(&quot;가입 실패 : 아이디를 확인해 주세요.&quot;);
            }
        })
        .catch(() =&gt; alert(&quot;서버 오류&quot;));
}</code></pre>
<table>
<thead>
<tr>
<th>단계</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>입력값 검증</td>
<td>빈 값이면 alert 후 종료</td>
</tr>
<tr>
<td><code>fetch</code> POST 요청</td>
<td>username, password, name을 JSON으로 전송</td>
</tr>
<tr>
<td>응답 처리</td>
<td><code>result === &quot;ok&quot;</code> 면 로그인 페이지로 이동</td>
</tr>
</tbody></table>
<hr>
<h2 id="4-로그인-백엔드">4. 로그인 백엔드</h2>
<h3 id="memberservice---로그인">MemberService - 로그인</h3>
<pre><code class="language-java">// 1. 아이디로 회원 조회
// 2. 비밀번호 일치하면 MemberVO 반환 (로그인 성공)
// 3. 아이디가 없거나 비밀번호 틀리면 null 반환 (로그인 실패)
public MemberVO login(String username, String password) {
    MemberVO member = memberMapper.findByUsername(username);
    if (member != null &amp;&amp; member.getPassword().equals(password)) {
        return member;
    }
    return null;
}</code></pre>
<h3 id="membercontroller---로그인">MemberController - 로그인</h3>
<pre><code class="language-java">// POST /api/member/login
// 요청 Body : {&quot;username&quot;: &quot;hong&quot;, &quot;password&quot;: &quot;1234&quot;}
// 응답(성공) : {&quot;result&quot;: &quot;ok&quot;, &quot;name&quot;: &quot;홍길동&quot;, &quot;role&quot;: &quot;USER&quot;, &quot;username&quot;: &quot;hong&quot;}
// 응답(실패) : {&quot;result&quot;: &quot;fail&quot;, &quot;name&quot;: null, &quot;role&quot;: null, &quot;username&quot;: null}
@PostMapping(&quot;/login&quot;)
public MemberLoginResponse login(@RequestBody MemberLoginRequest request) {
    MemberVO member = memberService.login(request.getUsername(), request.getPassword());
    if (member != null) {
        return new MemberLoginResponse(&quot;ok&quot;, member.getName(), member.getRole(), member.getUsername());
    } else {
        return new MemberLoginResponse(&quot;fail&quot;, null, null, null);
    }
}</code></pre>
<hr>
<h2 id="5-로그인-프론트">5. 로그인 프론트</h2>
<h3 id="loginhtml">login.html</h3>
<pre><code class="language-html">&lt;div class=&quot;auth-container&quot;&gt;
    &lt;div class=&quot;auth-box&quot;&gt;
        &lt;h2&gt;로그인&lt;/h2&gt;
        &lt;div class=&quot;form-group&quot;&gt;
            &lt;label&gt;아이디&lt;/label&gt;
            &lt;input type=&quot;text&quot; id=&quot;loginId&quot; placeholder=&quot;아이디를 입력하세요&quot; /&gt;
        &lt;/div&gt;
        &lt;div class=&quot;form-group&quot;&gt;
            &lt;label&gt;비밀번호&lt;/label&gt;
            &lt;input type=&quot;password&quot; id=&quot;loginPw&quot; placeholder=&quot;비밀번호를 입력하세요&quot; /&gt;
        &lt;/div&gt;
        &lt;button class=&quot;btn-submit&quot; onclick=&quot;login()&quot;&gt;로그인&lt;/button&gt;
        &lt;p class=&quot;auth-link&quot;&gt;
            계정이 없으신가요? &lt;a href=&quot;register.html&quot;&gt;회원가입&lt;/a&gt;
        &lt;/p&gt;
    &lt;/div&gt;
&lt;/div&gt;</code></pre>
<h3 id="authjs---로그인">auth.js - 로그인</h3>
<pre><code class="language-javascript">function login() {
    const id = document.getElementById(&quot;loginId&quot;).value.trim();
    const pw = document.getElementById(&quot;loginPw&quot;).value.trim();

    if (!id || !pw) {
        alert(&quot;아이디와 비밀번호를 입력해 주세요.&quot;);
        return;
    }

    fetch(`${AUTH_API}/login`, {
        method: &quot;POST&quot;,
        headers: { &quot;Content-Type&quot;: &quot;application/json&quot; },
        body: JSON.stringify({ username: id, password: pw }),
    })
        .then((res) =&gt; res.json())
        .then((data) =&gt; {
            if (data.result === &quot;ok&quot;) {
                localStorage.setItem(&quot;loginUser&quot;, data.name);
                localStorage.setItem(&quot;loginRole&quot;, data.role);
                localStorage.setItem(&quot;loginUserId&quot;, data.username);
                alert(`${data.name}님 환영합니다😊`);
                setTimeout(() =&gt; {
                    location.href = &quot;index.html&quot;;
                }, 1500);
            } else {
                alert(&quot;아이디 또는 비밀번호가 틀렸습니다.&quot;);
            }
        })
        .catch(() =&gt; alert(&quot;서버 오류&quot;));
}</code></pre>
<table>
<thead>
<tr>
<th>단계</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>입력값 검증</td>
<td>빈 값이면 alert 후 종료</td>
</tr>
<tr>
<td><code>fetch</code> POST 요청</td>
<td>username, password를 JSON으로 전송</td>
</tr>
<tr>
<td>로그인 성공</td>
<td>name, role, username을 localStorage에 저장 후 메인으로 이동</td>
</tr>
<tr>
<td>로그인 실패</td>
<td>오류 메시지 alert</td>
</tr>
</tbody></table>
<blockquote>
<p>로그인 성공 시 <code>localStorage</code>에 사용자 정보를 저장해두면 다른 페이지에서도 로그인 상태를 유지할 수 있다.</p>
</blockquote>
<hr>
<h2 id="6-공통-기능-commonjs">6. 공통 기능 (common.js)</h2>
<pre><code class="language-javascript">// nav 업데이트
function updateNav() {
    const isLoggedIn = localStorage.getItem(&quot;loginUser&quot;);
    const loginRole = localStorage.getItem(&quot;loginRole&quot;);
    const navArea = document.getElementById(&quot;navArea&quot;);

    if (isLoggedIn) {
        const adminMenu = loginRole === &quot;ADMIN&quot; ? `&lt;a href=&quot;admin.html&quot;&gt;메뉴 관리&lt;/a&gt;` : &quot;&quot;;
        const cartMenu = loginRole === &quot;USER&quot; ? `&lt;a href=&quot;cart.html&quot;&gt;장바구니&lt;/a&gt;` : &quot;&quot;;

        navArea.innerHTML = `
            &lt;a href=&quot;index.html&quot;&gt;메뉴&lt;/a&gt;
            ${adminMenu}
            ${cartMenu}
            &lt;a href=&quot;#&quot; onclick=&quot;logout()&quot;&gt;로그아웃&lt;/a&gt;
        `;
    }
}

// 로그아웃
function logout() {
    localStorage.clear();
    location.href = &quot;login.html&quot;;
}</code></pre>
<blockquote>
<p>로그인 상태면 역할에 따라 nav가 동적으로 바뀐다. ADMIN이면 메뉴 관리, USER면 장바구니가 추가된다.</p>
</blockquote>
<hr>
<h2 id="7-동작-확인">7. 동작 확인</h2>
<p>📸 <em>[스크린샷 - 회원가입 화면]</em>
아이디, 비밀번호, 이름을 입력하고 회원가입 버튼을 클릭하면 가입 완료 알림이 뜨고 자동으로 로그인 페이지로 이동한다. </p>
<p><img src="https://velog.velcdn.com/images/jhwest-dev/post/635ecf05-c3a4-4d7a-aa1b-674608ada6cb/image.png" alt=""></p>
<p>📸 *[스크린샷 - 로그인 화면]<br>*아이디와 비밀번호를 입력하고 로그인 버튼을 클릭하면 환영 메시지가 뜨고 메인 페이지로 이동한다.
<img src="https://velog.velcdn.com/images/jhwest-dev/post/a0eae89d-6867-43bb-8ae8-81aa5d147ff0/image.png" alt=""></p>
<p>📸 <em>[스크린샷 - 로그인 성공 후 nav 변경된 화면]</em>
로그인 성공 시 localStorage에 사용자 정보가 저장되고, nav가 역할에 따라 동적으로 변경된다. USER면 장바구니, ADMIN이면 메뉴 관리 링크가 추가된다.
<img src="https://velog.velcdn.com/images/jhwest-dev/post/58ec7245-e004-496a-a579-fedcb6867674/image.png" alt=""></p>
<hr>
<h2 id="마치며">마치며</h2>
<p>이번 글에서는 회원가입과 로그인 기능을 백엔드와 프론트엔드로 나눠서 구현해봤다.</p>
<ul>
<li>DTO로 요청/응답 데이터를 명확하게 분리</li>
<li>MyBatis로 DB 연동</li>
<li>localStorage로 로그인 상태 유지</li>
</ul>
<p>다음 편에서는 관리자 페이지에서 메뉴 CRUD 기능을 구현할 예정이다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[3편 - [Spring Boot] 피자 가게 - 프론트 연동하기 (HTML + CSS + JavaScript)]]></title>
            <link>https://velog.io/@jhwest-dev/Spring-Boot-%ED%94%BC%EC%9E%90-%EA%B0%80%EA%B2%8C-%ED%94%84%EB%A1%A0%ED%8A%B8-%EC%97%B0%EB%8F%99%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@jhwest-dev/Spring-Boot-%ED%94%BC%EC%9E%90-%EA%B0%80%EA%B2%8C-%ED%94%84%EB%A1%A0%ED%8A%B8-%EC%97%B0%EB%8F%99%ED%95%98%EA%B8%B0</guid>
            <pubDate>Mon, 18 May 2026 14:58:03 GMT</pubDate>
            <description><![CDATA[<h2 id="이-글에서-다룰-내용">이 글에서 다룰 내용</h2>
<ol>
<li>프로젝트 구조 설명</li>
<li>index.html 생성</li>
<li>style.css 생성</li>
<li>menu.js 생성</li>
<li>CRUD 기능 구현<ul>
<li>전체 메뉴 조회 (GET)</li>
</ul>
</li>
</ol>
<br>
---


<h2 id="1-프로젝트-구조-설명">1. 프로젝트 구조 설명</h2>
<pre><code>PIZZA-SHOP/
├── css/
│   └── style.css
├── js/
│   └── menu.js
└── index.html</code></pre><hr>
<h2 id="2-indexhtml-생성">2. index.html 생성</h2>
<ul>
<li>index.html은 피자 메뉴 목록을 보여주는 메인 페이지다.</li>
</ul>
<pre><code class="language-html">&lt;!doctype html&gt;
&lt;html lang=&quot;ko&quot;&gt;
    &lt;head&gt;
        &lt;meta charset=&quot;UTF-8&quot; /&gt;
        &lt;title&gt;PIZZA&lt;/title&gt;
        &lt;link rel=&quot;stylesheet&quot; href=&quot;css/style.css&quot; /&gt;
    &lt;/head&gt;
    &lt;body&gt;
        &lt;!-- 메뉴 목록 화면 --&gt;
        &lt;header&gt;
            &lt;h1&gt;PIZZA SHOP&lt;/h1&gt;
            &lt;nav id=&quot;navArea&quot;&gt;
                &lt;a href=&quot;index.html&quot;&gt;메뉴&lt;/a&gt;
                &lt;a href=&quot;login.html&quot;&gt;로그인&lt;/a&gt;
                &lt;a href=&quot;register.html&quot;&gt;회원가입&lt;/a&gt;
            &lt;/nav&gt;
        &lt;/header&gt;

        &lt;div class=&quot;hero&quot;&gt;
            &lt;h2&gt;오늘의 피자 한 입 🍕&lt;/h2&gt;
            &lt;p&gt;맛있는 피자를 즐겨보세요.&lt;/p&gt;
        &lt;/div&gt;

        &lt;div class=&quot;filter&quot;&gt;
            &lt;button class=&quot;active&quot; onclick=&quot;filterMenu(&#39;전체&#39;, this)&quot;&gt;전체&lt;/button&gt;
            &lt;button onclick=&quot;filterMenu(&#39;클래식&#39;, this)&quot;&gt;클래식&lt;/button&gt;
            &lt;button onclick=&quot;filterMenu(&#39;프리미엄&#39;, this)&quot;&gt;프리미엄&lt;/button&gt;
            &lt;button onclick=&quot;filterMenu(&#39;사이드&#39;, this)&quot;&gt;사이드&lt;/button&gt;
        &lt;/div&gt;

        &lt;div class=&quot;menu-grid&quot; id=&quot;menuGrid&quot;&gt;&lt;/div&gt;

        &lt;footer&gt;2026 Pizza Shop. All rights reserved.&lt;/footer&gt;
        &lt;script src=&quot;js/menu.js&quot;&gt;&lt;/script&gt;
    &lt;/body&gt;
&lt;/html&gt;</code></pre>
<table>
<thead>
<tr>
<th>태그</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><code>&lt;header&gt;</code></td>
<td>로고 + 네비게이션 메뉴</td>
</tr>
<tr>
<td><code>&lt;div class=&quot;hero&quot;&gt;</code></td>
<td>메인 배너 영역</td>
</tr>
<tr>
<td><code>&lt;div class=&quot;filter&quot;&gt;</code></td>
<td>카테고리 필터 버튼</td>
</tr>
<tr>
<td><code>&lt;div class=&quot;menu-grid&quot;&gt;</code></td>
<td>메뉴 카드가 동적으로 들어오는 영역</td>
</tr>
</tbody></table>
<blockquote>
<p><code>&lt;div class=&quot;menu-grid&quot; id=&quot;menuGrid&quot;&gt;</code> 는 비어있는 상태로 시작하고 menu.js에서 API를 호출해 동적으로 메뉴 카드를 채워준다.</p>
</blockquote>
<hr>
<h2 id="3-stylecss-생성">3. style.css 생성</h2>
<p>style.css는 피자 쇼핑몰의 전체 디자인을 담당한다.</p>
<pre><code class="language-css">/* 공통 스타일 */
:root {
    --color-bg: #fffdf0;
    --color-dark: #2b1f1d;
    --color-primary: #ff5252;
    --color-accent: #ffb74d;
    --color-light: #ffffff;
    --color-border: #f0e6db;
    --color-text: #4a3b32;
    --color-muted: #a1887f;
    --font-main: &quot;Ansungtangmyeon&quot;, &quot;Comic Sans MS&quot;, &quot;Apple SD Gothic Neo&quot;, sans-serif;
    --radius: 20px;
    --shadow: 0 8px 24px rgba(235, 190, 160, 0.2);
    --transition: 0.25s cubic-bezier(0.175, 0.885, 0.32, 1.275);
}</code></pre>
<blockquote>
<p>CSS 변수(<code>:root</code>)를 사용하면 색상, 그림자, 애니메이션 등을 한 곳에서 관리할 수 있어 유지보수가 편리하다.</p>
</blockquote>
<table>
<thead>
<tr>
<th>변수</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><code>--color-bg</code></td>
<td>페이지 배경색 (따뜻한 아이보리)</td>
</tr>
<tr>
<td><code>--color-accent</code></td>
<td>주요 포인트 색상 (주황)</td>
</tr>
<tr>
<td><code>--color-primary</code></td>
<td>강조 색상 (빨강)</td>
</tr>
<tr>
<td><code>--shadow</code></td>
<td>카드 그림자 스타일</td>
</tr>
<tr>
<td><code>--transition</code></td>
<td>호버 애니메이션 곡선</td>
</tr>
</tbody></table>
<hr>
<h3 id="헤더--네비게이션">헤더 / 네비게이션</h3>
<pre><code class="language-css">header {
    background: var(--color-bg);
    padding: 20px 40px;
    display: flex;
    justify-content: space-between;
    align-items: center;
}

nav a {
    color: var(--color-primary);
    text-decoration: none;
    margin-left: 20px;
    font-size: 15px;
    font-weight: bold;
}</code></pre>
<blockquote>
<p><code>display: flex</code> + <code>justify-content: space-between</code>으로 로고는 왼쪽, 네비게이션은 오른쪽에 배치한다.</p>
</blockquote>
<hr>
<h3 id="필터-버튼">필터 버튼</h3>
<pre><code class="language-css">.filter {
    display: flex;
    justify-content: center;
    gap: 10px;
    padding: 30px 20px 10px;
}

.filter button {
    padding: 8px 20px;
    border: 2px solid var(--color-accent);
    border-radius: 20px;
    background: var(--color-bg);
    color: var(--color-accent);
    cursor: pointer;
}

.filter button:hover,
.filter button.active {
    background: var(--color-accent);
    color: var(--color-bg);
}</code></pre>
<table>
<thead>
<tr>
<th>상태</th>
<th>스타일</th>
</tr>
</thead>
<tbody><tr>
<td>기본</td>
<td>흰 배경 + 주황 테두리</td>
</tr>
<tr>
<td>hover / active</td>
<td>주황 배경 + 흰 글씨</td>
</tr>
</tbody></table>
<hr>
<h3 id="메뉴-그리드">메뉴 그리드</h3>
<pre><code class="language-css">.menu-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(230px, 1fr));
    gap: 24px;
    padding: 20px 40px 60px;
    max-width: 1100px;
    margin: 0 auto;
}</code></pre>
<blockquote>
<p><code>repeat(auto-fill, minmax(230px, 1fr))</code>를 사용하면 별도의 미디어 쿼리 없이도 화면 크기에 따라 자동으로 열 수가 조정되는 반응형 그리드가 완성된다.</p>
</blockquote>
<hr>
<h3 id="메뉴-카드">메뉴 카드</h3>
<pre><code class="language-css">.menu-card {
    background: var(--color-light);
    border-radius: 12px;
    overflow: hidden;
    box-shadow: var(--shadow);
    transition: var(--transition);
}

.menu-card:hover {
    transform: translateY(-4px);
}

.menu-card .img-area {
    background: var(--color-light);
    height: 160px;
    overflow: hidden;
    text-align: center;
    line-height: 160px;
    font-size: 60px;
}</code></pre>
<table>
<thead>
<tr>
<th>속성</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><code>overflow: hidden</code></td>
<td>이미지가 카드 밖으로 삐져나오지 않게 함</td>
</tr>
<tr>
<td><code>transition</code></td>
<td>호버 시 부드러운 애니메이션</td>
</tr>
<tr>
<td><code>transform: translateY(-4px)</code></td>
<td>호버 시 카드가 살짝 위로 뜨는 효과</td>
</tr>
<tr>
<td><code>line-height: 160px</code></td>
<td>이미지 없을 때 이모지를 세로 중앙 정렬</td>
</tr>
</tbody></table>
<hr>
<hr>
<h2 id="4-menujs-생성">4. menu.js 생성</h2>
<p>menu.js는 API를 호출해 메뉴 데이터를 받아오고, 화면에 카드를 동적으로 렌더링하는 역할을 한다.</p>
<pre><code class="language-javascript">const API = &quot;http://localhost:8080/api/menu&quot;;
let allMenus = []; // 전체 메뉴 캐시

window.onload = function () {
    loadMenus();
    updateNav();
};</code></pre>
<blockquote>
<p><code>allMenus</code>에 전체 메뉴를 캐싱해두면 카테고리 필터링 시 API를 다시 호출하지 않아도 된다.</p>
</blockquote>
<hr>
<h3 id="전체-메뉴-불러오기">전체 메뉴 불러오기</h3>
<pre><code class="language-javascript">function loadMenus() {
    fetch(API)
        .then((res) =&gt; res.json())
        .then((data) =&gt; {
            allMenus = data;
            renderMenus(data);
        })
        .catch((err) =&gt; console.error(&quot;메뉴 불러오기 실패: &quot;, err));
}</code></pre>
<table>
<thead>
<tr>
<th>단계</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><code>fetch(API)</code></td>
<td>GET 요청으로 메뉴 목록을 가져옴</td>
</tr>
<tr>
<td><code>res.json()</code></td>
<td>응답을 JSON으로 파싱</td>
</tr>
<tr>
<td><code>allMenus = data</code></td>
<td>필터링에 사용할 전체 메뉴 캐싱</td>
</tr>
<tr>
<td><code>renderMenus(data)</code></td>
<td>화면에 카드 렌더링</td>
</tr>
</tbody></table>
<hr>
<h3 id="카드-렌더링">카드 렌더링</h3>
<pre><code class="language-javascript">function renderMenus(menus) {
    const grid = document.getElementById(&quot;menuGrid&quot;);
    grid.innerHTML = &quot;&quot;;

    if (menus.length === 0) {
        grid.innerHTML = &#39;&lt;p style=&quot;text-align:center; color:#999; padding:40px;&quot;&gt;메뉴가 없습니다.&lt;/p&gt;&#39;;
        return;
    }

    menus.forEach((menu) =&gt; {
        const card = document.createElement(&quot;div&quot;);
        card.className = &quot;menu-card&quot;;

        const imgContent = menu.imgUrl
            ? `&lt;img src=&quot;${menu.imgUrl}&quot; alt=&quot;${menu.name}&quot; style=&quot;width:100%; height:160px; object-fit:contain; display:block;&quot;&gt;`
            : getCategoryEmoji(menu.category);

        card.innerHTML = `
            &lt;div class=&quot;img-area&quot;&gt;${imgContent}&lt;/div&gt;
            &lt;div class=&quot;info&quot;&gt;
                &lt;div class=&quot;category&quot;&gt;${menu.category}&lt;/div&gt;
                &lt;h3&gt;${menu.name}&lt;/h3&gt;
                &lt;div class=&quot;price&quot;&gt;${menu.price.toLocaleString()}원&lt;/div&gt;
                &lt;div class=&quot;btn-group&quot;&gt;
                    &lt;button onclick=&quot;order(&#39;${menu.name}&#39;, ${menu.price})&quot;&gt;주문하기&lt;/button&gt;
                    &lt;button onclick=&quot;addToCart(${menu.id}, &#39;${menu.name}&#39;, ${menu.price})&quot;&gt;장바구니 담기&lt;/button&gt;
                &lt;/div&gt;
            &lt;/div&gt;
        `;
        grid.appendChild(card);
    });
}</code></pre>
<blockquote>
<p><code>imgUrl</code>이 있으면 이미지를, 없으면 카테고리별 이모지를 대신 보여준다.</p>
</blockquote>
<pre><code class="language-javascript">function getCategoryEmoji(category) {
    if (category == &quot;클래식&quot;) return &quot;⭐&quot;;
    if (category == &quot;프리미엄&quot;) return &quot;👑&quot;;
    if (category == &quot;사이드&quot;) return &quot;🍟&quot;;
}</code></pre>
<hr>
<h3 id="카테고리-필터링">카테고리 필터링</h3>
<pre><code class="language-javascript">function filterMenu(category, event) {
    document.querySelectorAll(&quot;.filter button&quot;).forEach((btn) =&gt; {
        btn.classList.remove(&quot;active&quot;);
    });
    event.target.classList.add(&quot;active&quot;);

    if (category === &quot;전체&quot;) {
        renderMenus(allMenus);
    } else {
        const filtered = allMenus.filter((m) =&gt; m.category === category);
        renderMenus(filtered);
    }
}</code></pre>
<table>
<thead>
<tr>
<th>단계</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><code>classList.remove(&quot;active&quot;)</code></td>
<td>모든 버튼에서 active 제거</td>
</tr>
<tr>
<td><code>event.target.classList.add(&quot;active&quot;)</code></td>
<td>클릭한 버튼에만 active 추가</td>
</tr>
<tr>
<td><code>allMenus.filter(...)</code></td>
<td>캐싱된 데이터에서 카테고리 필터링 (API 재호출 없음)</td>
</tr>
</tbody></table>
<hr>
<h2 id="5-crud-기능-구현---①-전체-메뉴-조회-get">5. CRUD 기능 구현 - ① 전체 메뉴 조회 (GET)</h2>
<h3 id="전체-메뉴-조회-get">전체 메뉴 조회 (GET)</h3>
<p>페이지가 로드되면 자동으로 API를 호출해 전체 메뉴를 가져온다.
<img src="https://velog.velcdn.com/images/jhwest-dev/post/6f15a30b-0bab-47fc-bcd1-8babca2bd267/image.png" alt=""></p>
<p>카테고리 버튼을 클릭하면 해당 카테고리만 필터링되어 보여진다.</p>
<p><img src="https://velog.velcdn.com/images/jhwest-dev/post/2b87126c-b1ad-4f9c-a28f-a191fb2dec81/image.png" alt=""></p>
<hr>
<h2 id="마치며">마치며</h2>
<p>이번 글에서는 피자 쇼핑몰 프론트엔드의 기본 구조를 만들어봤다.</p>
<ul>
<li>index.html로 화면 구조 잡기</li>
<li>style.css로 디자인 입히기</li>
<li>menu.js로 API 연동 및 동적 렌더링</li>
</ul>
<p>다음 편에서는 로그인/회원가입 기능을 구현할 예정이다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[2편 - [Spring Boot] 피자 가게 - 메뉴 CRUD 구현하기 (백엔드 + Postman 테스트)]]></title>
            <link>https://velog.io/@jhwest-dev/Spring-Boot-%ED%94%BC%EC%9E%90-%EA%B0%80%EA%B2%8C-CRUD-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@jhwest-dev/Spring-Boot-%ED%94%BC%EC%9E%90-%EA%B0%80%EA%B2%8C-CRUD-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0</guid>
            <pubDate>Thu, 14 May 2026 07:23:27 GMT</pubDate>
            <description><![CDATA[<hr>
<p><img src="https://velog.velcdn.com/images/jhwest-dev/post/f3ac4988-e02c-497a-98e0-0f02ead00c59/image.png" alt=""></p>
<p>학원에서 위 이미지와 같이 Spring Boot로 coffee shop 페이지를 만들어 보았다.<br>배운 내용을 제대로 익히기 위해 복습 겸 피자 가게 페이지를 직접 만들어보려 한다.</p>
<h2 id="이-글에서-다룰-내용">이 글에서 다룰 내용</h2>
<ul>
<li>피자 메뉴 전체 조회</li>
<li>메뉴 단건 조회</li>
<li>메뉴 등록</li>
<li>메뉴 수정</li>
<li>메뉴 삭제</li>
</ul>
<h2 id="사용-기술-스택">사용 기술 스택</h2>
<ul>
<li>Spring Boot</li>
<li>MySQL</li>
<li>Lombok</li>
<li>Postman</li>
<li>MyBatis</li>
</ul>
<hr>
<h2 id="1-mysql-에서-db-생성-및-테이블-생성">1. MySQL 에서 DB 생성 및 테이블 생성</h2>
<p><strong>1. 쿼리 작성 및 실행</strong></p>
<pre><code class="language-sql">-- pizza_db 생성
create database if not exists pizza_db default character set utf8mb4;

-- pizza_db 사용
use pizza_db;

-- menu 테이블 생성
create table menu(
    id int auto_increment primary key,
    name varchar(100) not null,
    size varchar(10) not null,
    price int not null,
    category varchar(50) not null,
    img_url varchar(255)
)
</code></pre>
<p><strong>2. pizza_db의 menu 테이블 생성 완료</strong>
<img src="https://velog.velcdn.com/images/jhwest-dev/post/92bf8cd9-c8df-4555-910e-77ce999cf3ad/image.png" alt=""></p>
<hr>
<h2 id="2-applicationproperties-설정-db-연결-mybatis">2. application.properties 설정 (DB 연결, MyBatis)</h2>
<pre><code class="language-java"># 프로젝트 이름
spring.application.name=pizza-shop

# DB 연결 설정
# url - 어떤 DB에 연결할지 (localhost:3306 = 내 컴퓨터의 MySql_db = 사용 할 DB 이름)
spring.datasource.url=jdbc:mysql://localhost:3306/pizza_db?useSSL=false&amp;serverTimezone=Asia/Seoul

# username/password = MySql 로그인 정보
spring.datasource.username=&lt;username&gt;
spring.datasource.password=&lt;password&gt;

# driver = Mysql 전용 드라이버 클래스 (Mysql 8.x 버전용)
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

# Mybatis 설정
# VO 클래스가 있는 패키지 등록 -&gt; 쿼리 결과를 VO 객체로 자동 변환해줌
mybatis.type-aliases-package=com.example.demo.vo
# DB 컬럼명 (img_url)을 Java 필드명(imgUrl)으로 자동 변환 (언더스코어 -&gt; 카멜케이스)
mybatis.configuration.map-underscore-to-camel-case=true
</code></pre>
<p><img src="https://velog.velcdn.com/images/jhwest-dev/post/76d16dc8-5334-4267-9548-0877ec9e036f/image.png" alt=""></p>
<hr>
<h2 id="3-프로젝트-생성-및-패키지-생성">3. 프로젝트 생성 및 패키지 생성</h2>
<ol>
<li>controller - 클라이언트 요청을 받고 응답을 반환</li>
<li>service - 비즈니스 로직 담당</li>
<li>vo - 데이터를 담는 객체 (DB 테이블과 매핑)</li>
<li>mapper - DB에 접근해서 쿼리 실행
<img src="https://velog.velcdn.com/images/jhwest-dev/post/5da3fa21-6ac9-4929-b51a-a042798d6301/image.png" alt=""></li>
</ol>
<p><img src="https://velog.velcdn.com/images/jhwest-dev/post/b0b190b1-a9a8-49b2-af7d-08b130fb90a5/image.png" alt=""></p>
<hr>
<h2 id="4-menuvo-클래스-생성">4. MenuVO 클래스 생성</h2>
<ul>
<li>DB 테이블과 매핑되는 데이터 객체<pre><code class="language-java">package com.pizzashop.vo;
</code></pre>
</li>
</ul>
<p>import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;</p>
<p>@Data
@NoArgsConstructor
@AllArgsConstructor
public class MenuVO {</p>
<pre><code>private int id; // 고유 아이디
private String name; // 메뉴명
private String size; // 피자 사이즈 
private int price; // 가격
private String category; // 카테고리
private String imgUrl; // 이미지 URL</code></pre><p>}</p>
<pre><code>| 어노테이션 | 기능 |
|-----------|------|
| `@Data` | getter, setter, toString 등을 자동 생성 |
| `@NoArgsConstructor` | 기본 생성자 자동 생성 (매개변수 없음) |
| `@AllArgsConstructor` | 전체 필드 생성자 자동 생성 (모든 필드 포함) |

#### @Data
```java
// 이걸 직접 안 써도 됨
public String getName() { return name; }
public void setName(String name) { this.name = name; }</code></pre><h4 id="noargsconstructor">@NoArgsConstructor</h4>
<pre><code class="language-java">// 이걸 자동 생성
public MenuVO() {}</code></pre>
<h4 id="allargsconstructor">@AllArgsConstructor</h4>
<pre><code class="language-java">// 이걸 자동 생성
public MenuVO(int id, String name, String size, int price, String category, String imgUrl) {}</code></pre>
<hr>
<h2 id="5-menumapper-인터페이스-생성">5. MenuMapper 인터페이스 생성</h2>
<h4 id="mapper를-인터페이스로-생성하는-이유">Mapper를 인터페이스로 생성하는 이유</h4>
<p>직접 구현 코드를 작성하지 않아도 MyBatis가 자동으로 구현체를 만들어주기 때문이다.
개발자는 어떤 쿼리를 실행할지만 정의하면 된다.</p>
<h4 id="mapper">@Mapper</h4>
<p>MyBatis에게 &quot;이 인터페이스가 Mapper입니다&quot; 라고 알려주는 어노테이션이다.
Spring이 자동으로 구현체를 생성하고 Bean으로 등록해준다.</p>
<h4 id="optionsusegeneratedkeys--true-keyproperty--id">@Options(useGeneratedKeys = true, keyProperty = &quot;id&quot;)</h4>
<p>INSERT 후 DB에서 자동 생성된 id 값을 MenuVO 객체에 자동으로 담아주는 옵션이다.</p>
<table>
<thead>
<tr>
<th>옵션</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><code>useGeneratedKeys = true</code></td>
<td>DB에서 자동 생성된 키(id)를 가져올지 여부</td>
</tr>
<tr>
<td><code>keyProperty = &quot;id&quot;</code></td>
<td>가져온 키를 어느 필드에 담을지 지정</td>
</tr>
</tbody></table>
<pre><code class="language-java">package com.pizzashop.mapper;

import java.util.List;

import org.apache.ibatis.annotations.Delete;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Options;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Update;

import com.pizzashop.vo.MenuVO;

@Mapper
public interface MenuMapper {

//    메뉴 전체 조회
    @Select(&quot;SELECT * FROM menu ORDER BY id ASC&quot;)
    List&lt;MenuVO&gt; findAll();

//    메뉴 단건 조회
    @Select(&quot;SELECT * FROM menu WHERE id = #{id}&quot;)
    MenuVO findById(int id);

//    메뉴 등록
    @Insert(&quot;INSERT INTO menu (name, size, price, category, img_url) VALUES (#{name}, #{size}, #{price}, #{category}, #{imgUrl})&quot;)
    @Options(useGeneratedKeys = true, keyProperty = &quot;id&quot;)
    boolean insert(MenuVO menu);

//    메뉴 수정
    @Update(&quot;UPDATE menu SET name=#{name}, size=#{size}, price=#{price}, category=#{category}, img_url=#{imgUrl} WHERE id=#{id}&quot;)
    boolean update(MenuVO menu);

//    메뉴 삭제
    @Delete(&quot;DELETE FROM menu WHERE id=#{id}&quot;)
    boolean delete(int id);

}
</code></pre>
<hr>
<h2 id="6-menuservice-클래스-생성">6. MenuService 클래스 생성</h2>
<h4 id="service-클래스란">Service 클래스란?</h4>
<p>Controller와 Mapper 사이에서 비즈니스 로직을 처리하는 계층이다.
Controller는 요청/응답만 담당하고, 실제 처리 로직은 Service에서 담당한다.</p>
<pre><code>클라이언트 → Controller → Service → Mapper → DB</code></pre><h4 id="autowired">@Autowired</h4>
<p>Spring이 자동으로 객체(Bean)를 찾아서 주입해주는 어노테이션이다.
직접 new로 객체를 생성할 필요 없이 Spring이 알아서 넣어준다.</p>
<pre><code class="language-java">// @Autowired 없으면 직접 생성해야 함
MenuMapper menuMapper = new MenuMapper();

// @Autowired 있으면 Spring이 알아서 주입
@Autowired
private MenuMapper menuMapper;</code></pre>
<h4 id="service">@Service</h4>
<p>Spring에게 &quot;이 클래스가 Service입니다&quot; 라고 알려주는 어노테이션이다.
자동으로 Bean으로 등록되어 @Autowired로 주입받을 수 있다.</p>
<pre><code class="language-java">package com.pizzashop.service;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import com.pizzashop.mapper.MenuMapper;
import com.pizzashop.vo.MenuVO;

@Service
public class MenuService {
// @Autowired = Spring이 알아서 MenuMapper 객체를 여기에 주입해줌(직접 new를 할 필요 없음)
        @Autowired
        private MenuMapper menuMapper;

//        전체 메뉴 조회
        public List&lt;MenuVO&gt; getAll() {return menuMapper.findAll();}

//        단건 메뉴 조회
        public MenuVO getById(int id) {return menuMapper.findById(id);}

//        메뉴 등록 / 수정 / 삭제
//        insert 결과가 1이상이면 true(성공), 0이면 false(실패)
        public boolean add(MenuVO menu) {return menuMapper.insert(menu);}
        public boolean update(MenuVO menu) {return menuMapper.update(menu);}
        public boolean delete(int id) {return menuMapper.delete(id);}

}
</code></pre>
<hr>
<h2 id="7-menuconroller-클래스-생성">7. MenuConroller 클래스 생성</h2>
<pre><code>package com.pizzashop.controller;

import java.util.List;
import java.util.Map;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.pizzashop.service.MenuService;
import com.pizzashop.vo.MenuVO;

@RestController
@RequestMapping(&quot;/api/menu&quot;)
@CrossOrigin(origins=&quot;*&quot;)
public class MenuController {

    @Autowired
    private MenuService menuService;

//    메뉴 조회
    @GetMapping
    public List&lt;MenuVO&gt; getAll() {
        return menuService.getAll();
    }

//    단건 조회
    @GetMapping(&quot;/{id}&quot;)
    public MenuVO getById(@PathVariable(&quot;id&quot;) int id) {
        return menuService.getById(id);
    }

//    메뉴 등록
    @PostMapping
    public Map&lt;String, Object&gt; add(@RequestBody MenuVO menu) {
        boolean success = menuService.add(menu);
        if(success) return Map.of(&quot;result&quot;, &quot;ok&quot;, &quot;message&quot;, &quot;등록 완료&quot;);
        return Map.of(&quot;result&quot;, &quot;fail&quot;, &quot;message&quot;, &quot;등록 실패&quot;);
    }

//    메뉴 수정
    @PutMapping(&quot;/{id}&quot;)
    public Map&lt;String, Object&gt; update(@PathVariable(&quot;id&quot;) int id, @RequestBody MenuVO menu) {
        menu.setId(id);
        boolean success = menuService.update(menu);
        if(success) return Map.of(&quot;result&quot;, &quot;ok&quot;, &quot;message&quot;, &quot;수정 완료&quot;);
        return Map.of(&quot;result&quot;, &quot;fail&quot;, &quot;message&quot;, &quot;수정 실패&quot;);
    }

//    메뉴 삭제
    @DeleteMapping(&quot;/{id}&quot;)
    public Map&lt;String, Object&gt; delete(@PathVariable(&quot;id&quot;) int id) {
        boolean success = menuService.delete(id);
        if(success) return Map.of(&quot;result&quot;, &quot;ok&quot;, &quot;message&quot;, &quot;삭제 완료&quot;);
        return Map.of(&quot;result&quot;, &quot;fail&quot;, &quot;message&quot;, &quot;삭제 실패&quot;);
    }
}</code></pre><hr>
<h2 id="8-postman으로-api-실행해보기">8. Postman으로 API 실행해보기</h2>
<ol>
<li>메뉴 등록
<em>(이미지 URL은 피자스쿨의 이미지 URL 사용)</em></li>
</ol>
<p><img src="https://velog.velcdn.com/images/jhwest-dev/post/5d299a97-b0f6-4937-848b-08576ce24a2e/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/jhwest-dev/post/b7c57e5b-4f7e-4604-8c72-ee392d0ba18f/image.png" alt="">
2. 전체 메뉴 조회
<img src="https://velog.velcdn.com/images/jhwest-dev/post/04edb70c-32ac-44f9-b9ca-81b309d72533/image.png" alt="">
3. 단건 메뉴 조회
<img src="https://velog.velcdn.com/images/jhwest-dev/post/7a11ea8f-d025-40a7-b9aa-c688691c6d66/image.png" alt="">
4. 메뉴 수정
<img src="https://velog.velcdn.com/images/jhwest-dev/post/a7ee5ada-6728-42b0-8b22-2e10283e8e60/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/jhwest-dev/post/2a22cb43-576d-407e-9d2e-a5c32b17c77d/image.png" alt="">
5. 메뉴 삭제
<img src="https://velog.velcdn.com/images/jhwest-dev/post/1ed3fef2-ea05-4129-ad1c-c04b79aee674/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/jhwest-dev/post/e3ecff35-ec24-4582-b634-e58054116d67/image.png" alt=""></p>
<hr>
<h2 id="마무리">마무리</h2>
<p>이번 글에서는 피자 가게 메뉴 CRUD를 직접 구현해봤다.</p>
<p>학원에서 배운 내용을 다시 만들어보니 전체적인 흐름이 더 명확하게 이해됐다.</p>
<pre><code>클라이언트 → Controller → Service → Mapper → DB</code></pre><p>VO, Mapper, Service, Controller 각각의 역할을 이해하고 나니 Spring Boot의 구조가 훨씬 익숙하게 느껴졌다.</p>
<p>다음 글에서는 피자 가게 프론트 연동을 해볼 예정이다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[1편 - [Spring Boot] 개발 환경 세팅 (feat. MySQL, JDK, Lombok, Postman)]]></title>
            <link>https://velog.io/@jhwest-dev/Spring-Boot-%EA%B0%9C%EB%B0%9C-%ED%99%98%EA%B2%BD-%EC%84%B8%ED%8C%85%EB%B6%80%ED%84%B0-CRUD-%EA%B5%AC%ED%98%84%EA%B9%8C%EC%A7%80-feat.-MySQL-MyBatis-Lombok-Postman</link>
            <guid>https://velog.io/@jhwest-dev/Spring-Boot-%EA%B0%9C%EB%B0%9C-%ED%99%98%EA%B2%BD-%EC%84%B8%ED%8C%85%EB%B6%80%ED%84%B0-CRUD-%EA%B5%AC%ED%98%84%EA%B9%8C%EC%A7%80-feat.-MySQL-MyBatis-Lombok-Postman</guid>
            <pubDate>Wed, 13 May 2026 16:33:27 GMT</pubDate>
            <description><![CDATA[<h1 id="개발-환경-세팅">개발 환경 세팅</h1>
<h2 id="1-jdk-jdk-17-lts-버전-설치">1. JDK (JDK 17-LTS 버전) 설치</h2>
<ol>
<li><p>아래 사이트 접속 &gt; Other Downloads 클릭 
<a href="https://adoptium.net/">https://adoptium.net/</a>
<img src="https://velog.velcdn.com/images/jhwest-dev/post/d9f8d900-c72a-4b2c-abc5-0cef8c41a0ae/image.png" alt=""></p>
</li>
<li><p>JDK 17-LTS 탭 클릭 &gt; 운영체제에 맞는 JDK 설치
<img src="https://velog.velcdn.com/images/jhwest-dev/post/313a5fed-7af3-4dd3-94e0-e7fdcc4a651a/image.png" alt=""></p>
</li>
</ol>
<hr>
<h2 id="2-스프링부트-설치">2. 스프링부트 설치</h2>
<ol>
<li><p>아래 사이트 접속 &gt; ECLIPSE 탭 선택 &gt; 운영체제에 맞는 프로그램 설치
<a href="https://spring.io/tools">https://spring.io/tools</a>
<img src="https://velog.velcdn.com/images/jhwest-dev/post/bb44b78c-0043-4aad-a1fd-7560e3e7ca24/image.png" alt=""></p>
</li>
<li><p>압축 풀고  → 경로 설정 - Launch
<img src="https://velog.velcdn.com/images/jhwest-dev/post/08d3c6d6-715e-4fd5-990b-9401e288d077/image.png" alt=""></p>
</li>
</ol>
<hr>
<h2 id="3-롬복-설치">3. 롬복 설치</h2>
<ol>
<li><p>아래 사이트 접속 &gt; 파일 다운로드
<a href="https://projectlombok.org/download">https://projectlombok.org/download</a>
<img src="https://velog.velcdn.com/images/jhwest-dev/post/8b755795-7bd4-4767-931c-bf0051275930/image.png" alt=""></p>
</li>
<li><p>Specify locatio...버튼 클릭 &gt; 이전에 설치한 SpringToolsForEclipse.exe 파일 선택 &gt; instaal / Up... 버튼 클릭 &gt; Quit Inst... 버튼 클릭
<img src="https://velog.velcdn.com/images/jhwest-dev/post/31d5881d-249d-4f41-8033-fa9b8e2153d3/image.png" alt=""></p>
<br>
## ⚠️ 롬복 설치 시 아래와 같은 에러가 난다면?
![](https://velog.velcdn.com/images/jhwest-dev/post/c18922a7-c71c-4e55-bf0a-d7ada61647eb/image.png)
</li>
<li><p>관리자 권한으로 cmd창 열기 &gt; lombok.jar가 있는 디렉토리로 이동
<img src="https://velog.velcdn.com/images/jhwest-dev/post/e5d5d7fc-cee7-40b4-99cb-936d1e4b8cf6/image.png" alt="">
<img src="https://velog.velcdn.com/images/jhwest-dev/post/af11c49a-7473-49d4-87fc-bb0f9cea0b3d/image.png" alt=""></p>
</li>
<li><p>java -jar lombok.jar 명령어 실행 &gt; 설치 재시도
<img src="https://velog.velcdn.com/images/jhwest-dev/post/a263dcc1-3b61-488f-9ae4-715b560135b3/image.png" alt=""></p>
</li>
</ol>
<hr>
<h2 id="4-mysql-설치">4. MySQL 설치</h2>
<ol>
<li><p>아래 사이트 접속 &gt; 다운로드 버튼 클릭
<a href="https://dev.mysql.com/downloads/installer/">https://dev.mysql.com/downloads/installer/</a>
<img src="https://velog.velcdn.com/images/jhwest-dev/post/3343e1aa-65b6-4422-a0bd-84296d1fa480/image.png" alt=""></p>
</li>
<li><p>Custom &gt; Next 클릭
<img src="https://velog.velcdn.com/images/jhwest-dev/post/3ccd3957-2dbc-4fa8-93d3-93ae054925e4/image.png" alt=""></p>
</li>
<li><p>왼쪽 Available Products에서 오른쪽으로 이동 &gt; Next
✅ MySQL Server 8.0.46 - X64
✅ MySQL Workbench 8.0.47 - X64</p>
</li>
</ol>
<p><img src="https://velog.velcdn.com/images/jhwest-dev/post/fad3def9-d3ae-4e98-b503-8b05b627a6c8/image.png" alt=""></p>
<ol start="4">
<li><p>설치 완료 후 &gt; Execute 클릭
<img src="https://velog.velcdn.com/images/jhwest-dev/post/f6b35834-fac6-4dcf-8fe8-f85385db3e1a/image.png" alt=""></p>
</li>
<li><p>root 계정의 비밀번호를 설정 
(이 비밀번호는 나중에 DB 접속 시 반드시 필요하므로 반드시 기억/기록해두기)
<img src="https://velog.velcdn.com/images/jhwest-dev/post/2e1bd715-0e1c-42be-8e97-2e111de7837d/image.png" alt=""></p>
</li>
<li><p>이후 나오는 화면들은 모두 기본값 유지 후 Next 클릭</p>
<br>

</li>
</ol>
<h2 id="⚠️-mysql-삭제-후-재설치-시-주의사항">⚠️ MySQL 삭제 후 재설치 시 주의사항</h2>
<p>단순히 프로그램만 제거하면 재설치 시 오류가 발생할 수 있다.
<strong>아래 순서대로</strong> 완전히 삭제 후 재설치</p>
<h3 id="1-프로그램-제거">1. 프로그램 제거</h3>
<p><strong>제어판 → 프로그램 추가/제거</strong>에서 아래 항목을 모두 제거</p>
<ul>
<li>MySQL Server</li>
<li>MySQL Workbench</li>
<li>MySQL Installer</li>
</ul>
<h3 id="2-잔여-파일-삭제">2. 잔여 파일 삭제</h3>
<pre><code>C:\Program Files\MySQL
C:\ProgramData\MySQL</code></pre><blockquote>
<p>💡 <code>ProgramData</code>는 숨김 폴더
탐색기 상단 <strong>보기 → 숨긴 항목 체크</strong> 후 접근</p>
</blockquote>
<h3 id="3-재부팅-후-재설치-진행-✅">3. 재부팅 후 재설치 진행 ✅</h3>
<hr>
<h2 id="5-postman-설치">5. Postman 설치</h2>
<ol>
<li>아래 사이트 접속 &gt; 다운로드 &gt; 로그인 후 실행
<a href="https://www.postman.com/downloads/">https://www.postman.com/downloads/</a>
<img src="https://velog.velcdn.com/images/jhwest-dev/post/98ae3a97-91db-4747-ae2d-12ab4dc14e08/image.png" alt=""><br>
## ⚠️ Postman 삭제 후 재설치 시 주의사항

</li>
</ol>
<p>단순히 프로그램만 제거하면 재설치 시 오류가 발생할 수 있다.
<strong>아래 순서대로</strong> 완전히 삭제 후 재설치</p>
<h3 id="1-프로그램-제거-1">1. 프로그램 제거</h3>
<p><strong>제어판 → 프로그램 추가/제거</strong>에서 아래 항목을 제거</p>
<ul>
<li>Postman</li>
</ul>
<h3 id="2-잔여-파일-삭제-1">2. 잔여 파일 삭제</h3>
<pre><code>C:\Users\[사용자명]\AppData\Roaming\Postman
C:\Users\[사용자명]\AppData\Local\Postman</code></pre><blockquote>
<p>💡 <code>AppData</code>는 숨김 폴더!
탐색기 상단 <strong>보기 → 숨긴 항목 체크</strong> 후 접근</p>
</blockquote>
<h3 id="3-재설치-진행-✅">3. 재설치 진행 ✅</h3>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Java] 백트래킹 - 부분집합과 순열]]></title>
            <link>https://velog.io/@jhwest-dev/Java-%EB%B0%B1%ED%8A%B8%EB%9E%98%ED%82%B9-%EB%B6%80%EB%B6%84%EC%A7%91%ED%95%A9%EA%B3%BC-%EC%88%9C%EC%97%B4</link>
            <guid>https://velog.io/@jhwest-dev/Java-%EB%B0%B1%ED%8A%B8%EB%9E%98%ED%82%B9-%EB%B6%80%EB%B6%84%EC%A7%91%ED%95%A9%EA%B3%BC-%EC%88%9C%EC%97%B4</guid>
            <pubDate>Fri, 08 May 2026 13:54:39 GMT</pubDate>
            <description><![CDATA[<h2 id="부분집합-vs-순열-vs-조합">부분집합 vs 순열 vs 조합</h2>
<table>
<thead>
<tr>
<th></th>
<th>순서</th>
<th>개수</th>
</tr>
</thead>
<tbody><tr>
<td>부분집합</td>
<td>상관 없음</td>
<td>상관 없음</td>
</tr>
<tr>
<td>순열</td>
<td>상관 있음</td>
<td>r개</td>
</tr>
<tr>
<td>조합</td>
<td>상관 없음</td>
<td>r개</td>
</tr>
</tbody></table>
<h3 id="예시-1-2-3-에서-2개-뽑기">예시 {1, 2, 3} 에서 2개 뽑기</h3>
<p><strong>순열</strong></p>
<ul>
<li>가짓수 : 6개</li>
<li>예시 : {1,2}, {1,3}, {2,1}, {2,3}, {3,1}, {3,2}</li>
</ul>
<p><strong>조합</strong></p>
<ul>
<li>가짓수 : 3개</li>
<li>예시 : {1,2}, {1,3}, {2,3}</li>
</ul>
<h3 id="한-줄-요약">한 줄 요약</h3>
<ul>
<li>부분집합 : 각 원소를 포함할지 말지 결정하며 나올 수 있는 모든 집합 (빈 집합 포함!)</li>
<li>순열 : r개의 원소를 나열 (순서 중요!)</li>
<li>조합 : r개의 원소를 나열 (순서는 중요하지 않음)</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Java] 그리디(Greedy) 알고리즘 개념 정리]]></title>
            <link>https://velog.io/@jhwest-dev/Java-%EA%B7%B8%EB%A6%AC%EB%94%94Greedy-%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98-%EA%B0%9C%EB%85%90-%EC%A0%95%EB%A6%AC</link>
            <guid>https://velog.io/@jhwest-dev/Java-%EA%B7%B8%EB%A6%AC%EB%94%94Greedy-%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98-%EA%B0%9C%EB%85%90-%EC%A0%95%EB%A6%AC</guid>
            <pubDate>Thu, 30 Apr 2026 08:41:41 GMT</pubDate>
            <description><![CDATA[<h2 id="그리디greedy-알고리즘이란">그리디(Greedy) 알고리즘이란?</h2>
<blockquote>
<p>&#39;탐욕스러운 알고리즘&#39; :  나중을 생각하지 않고 지금 할 수 있는 최선의 선택을 한다.</p>
</blockquote>
<p><strong>특징</strong></p>
<ul>
<li><strong>빠르다</strong> : 모든 경우를 탐색하지 않음</li>
<li><strong>항상 최적해를 보장하진 않음</strong></li>
<li><strong>특정 조건이 성립할 때만 사용 가능</strong><ul>
<li>조건은 문제마다 다르며, 문제를 풀면서 감을 익혀야 함</li>
<li>ex) 동전 문제 : 큰 동전이 작은 동전의 배수 관계일 때 성립</li>
</ul>
</li>
</ul>
<hr>
<h2 id="예제-1-동전-거스름돈">예제 1) 동전 거스름돈</h2>
<p>동전 <code>{500, 100, 50, 10, 5, 1}</code> 으로 <code>1200원</code>을 거슬러줄 때 최소 동전 개수를 구하라.</p>
<h3 id="코드">코드</h3>
<pre><code class="language-java">public class GreedyPractice {
    static void main(String[] args) {
        int[] moneyList = {500, 100, 50, 10, 5, 1};
        int amount = 1200;
        int totalCount = 0;
        for (int money : moneyList) {
            int count = amount / money;
            if (count &gt; 0) {
                System.out.println(money + &quot;짜리 동전 X &quot; + count + &quot;개&quot;);
                totalCount += count;
                amount -= money * count;
            }
            if (amount == 0) break;
        }
        System.out.println(&quot;최소 동전 개수 : &quot; + totalCount + &quot;개&quot;);
    }
}</code></pre>
<h3 id="출력">출력</h3>
<pre><code>500짜리 동전 X 2개
100짜리 동전 X 2개
최소 동전 개수 : 4개</code></pre><h3 id="동작-원리">동작 원리</h3>
<p>내림차순으로 정렬된 동전 배열을 반복문으로 돌려 제일 큰 동전부터 선택한다.</p>
<pre><code class="language-java">int count = amount / money;  // 현재 동전으로 줄 수 있는 최대 개수</code></pre>
<p>→ 1200 / 500 = <strong>2개</strong></p>
<pre><code class="language-java">totalCount += count;         // 총 개수에 더하기
amount -= money * count;     // 금액에서 빼기</code></pre>
<p>→ totalCount = 2, amount = 1200 - 1000 = <strong>200원 남음</strong></p>
<p>다음 동전 100원이 들어오면</p>
<p>→ 200 / 100 = <strong>2개</strong>
→ totalCount = 4, amount = 200 - 200 = <strong>0원</strong></p>
<pre><code class="language-java">if (amount == 0) break;      // 금액이 0이면 종료</code></pre>
<p>→ 반복문 종료, 최소 동전 개수 <strong>4개</strong> 출력</p>
<hr>
<h2 id="예제-2-최대-회의-수-구하기">예제 2) 최대 회의 수 구하기</h2>
<p><strong>문제</strong> : 하나의 회의실에서 겹치지 않게 최대한 많은 회의를 열어라. 각 회의는 <code>{시작 시간, 종료 시간}</code> 으로 주어진다.</p>
<h3 id="코드-1">코드</h3>
<pre><code class="language-java">int[][] meetings = {
        {1, 4}, {3, 5}, {0, 6}, {5, 9}, {3, 7},
        {5, 9}, {6, 10}, {8, 11}, {8, 12}, {2, 14}, {12, 16}
};

Arrays.sort(meetings, (a, b) -&gt; a[1] - b[1]);

int count = 0;
int lastEnd = 0;
for (int[] meeting : meetings) {
    int start = meeting[0];
    int end = meeting[1];

    if (lastEnd &lt;= start) {
        lastEnd = end;
        count += 1;
        System.out.println(count + &quot;번째 회의! 시작 시간 : &quot; + start + &quot;, 끝 시간 : &quot; + end);
    }
}
System.out.println(&quot;총 회의 수 : &quot; + count);</code></pre>
<h3 id="출력-1">출력</h3>
<pre><code>1번째 회의! 시작 시간 : 1, 끝 시간 : 4
2번째 회의! 시작 시간 : 5, 끝 시간 : 9
3번째 회의! 시작 시간 : 12, 끝 시간 : 16
총 회의 수 : 3</code></pre><h3 id="동작-원리-1">동작 원리</h3>
<p>종료 시간이 짧을수록 새로운 회의를 더 많이 열 수 있으므로 종료 시간을 기준으로 오름차순 정렬한다.</p>
<pre><code class="language-java">Arrays.sort(meetings, (a, b) -&gt; a[1] - b[1]);</code></pre>
<p><code>a[1] - b[1]</code> 의 반환값으로 정렬 방향을 결정한다.</p>
<ul>
<li><strong>양수</strong> → 두 요소의 자리를 바꿈</li>
<li><strong>음수</strong> → 자리를 바꾸지 않음</li>
</ul>
<pre><code>a = 5, b = 6 → 5 - 6 = -1 (음수) → 자리 유지 → 오름차순
a = 6, b = 5 → 6 - 5 =  1 (양수) → 자리 바꿈 → 오름차순</code></pre><p>그러므로 a[1] - b[1]는 어떤 값이 들어와도 항상 오름차순이 된다.
내림차순을 하고 싶으면 b[1] - a[1] 이와 같이 작성하면 된다. </p>
<p>정렬 후 반복문으로 종료 시간이 짧은 회의부터 가져온다.</p>
<pre><code class="language-java">if (lastEnd &lt;= start) {
    lastEnd = end;
    count += 1;
}</code></pre>
<p>이전 회의 종료 시간(<code>lastEnd</code>)과 현재 회의 시작 시간(<code>start</code>)을 비교해서 현재 회의 시작 시간이 같거나 늦다면 회의를 시작할 수 있으므로 <code>count</code> 를 1 증가시키고 <code>lastEnd</code> 를 현재 회의 종료 시간으로 갱신한다.</p>
<hr>
<h3 id="마무리">마무리</h3>
<p>코드를 보고 선생님의 설명을 들을 때는 이해가 됐다고 생각했는데, 막상 문제만 놓고 혼자 풀어보려니 쉽지 않았다.
그리디는 단순히 코드를 외우는 게 아니라 문제마다 어떤 기준이 최선인지를 파악하는 게 핵심인 것 같다.
앞으로 문제를 많이 풀어보면서 감을 익혀야겠다. 💪</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Java] 트리(Tree) 자료구조 개념 정리]]></title>
            <link>https://velog.io/@jhwest-dev/Java-%ED%8A%B8%EB%A6%ACTree-%EC%9E%90%EB%A3%8C%EA%B5%AC%EC%A1%B0-%EA%B0%9C%EB%85%90-%EC%A0%95%EB%A6%AC</link>
            <guid>https://velog.io/@jhwest-dev/Java-%ED%8A%B8%EB%A6%ACTree-%EC%9E%90%EB%A3%8C%EA%B5%AC%EC%A1%B0-%EA%B0%9C%EB%85%90-%EC%A0%95%EB%A6%AC</guid>
            <pubDate>Wed, 29 Apr 2026 08:50:04 GMT</pubDate>
            <description><![CDATA[<h2 id="트리란">트리란?</h2>
<p>계층적 구조를 표현하는 <strong>비선형 자료구조</strong></p>
<p><strong>예시</strong></p>
<ol>
<li>회사 조직도</li>
<li>가족 관계도 등<br>

</li>
</ol>
<hr>
<h2 id="트리-용어-정리">트리 용어 정리</h2>
<p><img src="https://velog.velcdn.com/images/jhwest-dev/post/4200e840-782b-4b86-9bda-1363c31ffc0c/image.png" alt="트리 구조도"></p>
<ol>
<li><strong>노드(Node)</strong> : 트리의 각 원소</li>
<li><strong>엣지(Edge)</strong> : 노드를 연결하는 선, 간선</li>
<li><strong>루트(Root)</strong> : 최상위 노드</li>
<li><strong>부모(Parent)</strong> : 바로 위에 연결된 노드</li>
<li><strong>자식(Child)</strong> : 바로 아래에 연결된 노드</li>
<li><strong>형제(Sibling)</strong> : 같은 부모를 가진 노드</li>
<li><strong>리프(Leaf)</strong> : 자식이 없는 노드</li>
<li><strong>내부 노드(Internal Node)</strong> : 자식이 있는 노드</li>
<li><strong>깊이(Depth)</strong> : 루트에서 해당 노드까지의 엣지 수<ul>
<li>ex) A의 깊이 = 0, B의 깊이 = 1, D의 깊이 = 2</li>
</ul>
</li>
<li><strong>레벨(Level)</strong> : 깊이와 동일하게 쓰이거나 깊이 + 1로 쓰임</li>
<li><strong>높이(Height)</strong> : 트리 전체의 깊이 최댓값</li>
<li><strong>서브트리(Subtree)</strong> : 특정 노드를 루트로 하는 트리</li>
</ol>
<br>


<hr>
<h2 id="트리-직접-구현해보기">트리 직접 구현해보기</h2>
<p>아래 트리 구조를 코드로 구현하고 출력해보자.</p>
<pre><code>        1
      / | \
     2  3  4
    / \    |
   5  6    7</code></pre><h3 id="코드">코드</h3>
<pre><code class="language-java">import java.util.ArrayList;
import java.util.List;

public class TreeConceptPractice {

    static class Tree {
        int data;
        List&lt;Tree&gt; children;

        Tree(int data) {
            this.data = data;
            this.children = new ArrayList&lt;&gt;();
        }

        void addChild(Tree child) { this.children.add(child); }
    }

    static void printTree(Tree node, String prefix) {
        if (node == null) return;
        System.out.println(prefix + node.data);
        for (Tree child : node.children) {
            printTree(child, prefix + &quot;    &quot;);
        }
    }

    static void main(String[] args) {
        Tree root = new Tree(1);
        Tree n2 = new Tree(2);
        Tree n3 = new Tree(3);
        Tree n4 = new Tree(4);
        Tree n5 = new Tree(5);
        Tree n6 = new Tree(6);
        Tree n7 = new Tree(7);

        root.addChild(n2);
        root.addChild(n3);
        root.addChild(n4);
        n2.addChild(n5);
        n2.addChild(n6);
        n4.addChild(n7);

        printTree(root, &quot;&quot;);
    }
}</code></pre>
<h3 id="출력">출력</h3>
<pre><code>1
    2
        5
        6
    3
    4
        7</code></pre><h3 id="동작-원리">동작 원리</h3>
<p><code>printTree(root, &quot;&quot;)</code>가 호출되면 <code>&quot;&quot; + 1</code>이 출력된다.
이후 반복문으로 루트(1)의 자식들을 순서대로 순회하는데, 각 자식마다 재귀함수가 실행된다.</p>
<p>1의 자식은 2, 3, 4 순서로 처리되며, 2가 넘어가면 2의 자식인 5, 6이 먼저 출력된다.
2가 끝나면 3, 그다음 4와 4의 자식인 7 순서로 출력된다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Java] 완전 탐색 알고리즘 (Brute Force)]]></title>
            <link>https://velog.io/@jhwest-dev/Java-%EC%99%84%EC%A0%84-%ED%83%90%EC%83%89-%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98-Brute-Force</link>
            <guid>https://velog.io/@jhwest-dev/Java-%EC%99%84%EC%A0%84-%ED%83%90%EC%83%89-%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98-Brute-Force</guid>
            <pubDate>Tue, 28 Apr 2026 08:52:17 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>가능한 모든 경우의 수를 전부 시도해보는 알고리즘</p>
</blockquote>
<p>✅ <strong>장점</strong> : 정확성 100% 보장</p>
<p>❌ <strong>단점</strong> : 경우의 수가 많으면 시간이 오래 걸림</p>
<hr>
<h3 id="언제-사용하는가">언제 사용하는가?</h3>
<ul>
<li>경우의 수가 충분히 작을 때 (보통 1,000만 이하)</li>
<li>더 효율적인 알고리즘이 떠오르지 않을 때</li>
<li>정답을 먼저 구한 뒤 최적화할 때</li>
</ul>
<hr>
<h3 id="시간복잡도-기준">시간복잡도 기준</h3>
<ul>
<li>반복문 1개 : <code>O(n)</code> → n이 1억이면 약 0.1초</li>
<li>반복문 2중 : <code>O(n²)</code> → n이 1만이면 약 0.1초</li>
<li>반복문 3중 : <code>O(n³)</code> → n이 500이면 약 0.1초</li>
</ul>
<hr>
<h2 id="예제-1-3자리-영문-소문자-비밀번호">예제 1) 3자리 영문 소문자 비밀번호</h2>
<p>3자리 영문 소문자 (a ~ z)를 완전 탐색으로 모두 출력하라. 총 몇 가지인지도 출력하라.</p>
<h3 id="코드">코드</h3>
<pre><code class="language-java">public class BruteForcePractice {
    static void main(String[] args) {
        int count = 0;
        for (char i = &#39;a&#39;; i &lt;= &#39;z&#39;; i++) {
            for (char j = &#39;a&#39;; j &lt;= &#39;z&#39;; j++) {
                for (char k = &#39;a&#39;; k &lt;= &#39;z&#39;; k++) {
                    count++;
                    if (count &lt;= 3) {
                        System.out.println(&quot;&quot; + i + j + k);
                    } else if (count == 4) {
                        System.out.println(&quot;...&quot;);
                    } else if (count == 17576) {
                        System.out.println(&quot;&quot; + i + j + k);
                    }
                }
            }
        }
        System.out.println(&quot;총 : &quot; + count + &quot;가지&quot;);
    }
}</code></pre>
<h3 id="출력">출력</h3>
<pre><code>aaa
aab
aac
...
zzz
총 : 17576가지</code></pre><h3 id="동작-원리">동작 원리</h3>
<p>완전탐색은 가능한 모든 경우의 수를 다 구하는 방법이다.
3자리 비밀번호를 구해야 하므로 반복문을 3중으로 중첩해서 처음(a)부터 끝(z)까지 모든 경우를 순회하면 된다.</p>
<hr>
<h2 id="예제-2-두-수의-합">예제 2) 두 수의 합</h2>
<p>배열 <code>{1, 7, 11, 15, 3, 6}</code> 에서 두 수의 합이 <code>9</code>가 되는 쌍을 찾아라.</p>
<h3 id="코드-1">코드</h3>
<pre><code class="language-java">int[] arr = {1, 7, 11, 15, 3, 6};
int sumCount = 0;
for (int i = 0; i &lt; arr.length; i++) {
    for (int j = i + 1; j &lt; arr.length; j++) {
        if (arr[i] + arr[j] == 9) {
            sumCount++;
            System.out.println(arr[i] + &quot; + &quot; + arr[j] + &quot; = &quot; + (arr[i] + arr[j]));
        }
    }
}
System.out.println(&quot;총 : &quot; + sumCount + &quot;가지&quot;);</code></pre>
<h3 id="출력-1">출력</h3>
<pre><code>3 + 6 = 9
총 : 1가지</code></pre><h3 id="동작-원리-1">동작 원리</h3>
<p>두 수의 합이 9가 되는 쌍을 찾아야 하기에 반복문은 2중으로 사용하였다.
<code>j</code>를 <code>i+1</code>로 설정한 이유는 <code>j</code>가 항상 <code>i</code>보다 크기 때문에 <code>(3, 6)</code>과 <code>(6, 3)</code> 같이 순서만 다른 중복 쌍이 생기지 않는다.</p>
<hr>
<h2 id="예제-3-두-수의-차이">예제 3) 두 수의 차이</h2>
<p>배열 <code>{3, 10, 2, 8, 5, 1}</code> 에서 두 수의 차이가 가장 큰 쌍을 찾아라.</p>
<h3 id="코드-2">코드</h3>
<pre><code class="language-java">int[] arr2 = {3, 10, 2, 8, 5, 1};
int answer = Integer.MIN_VALUE;
int indexI = 0;
int indexJ = 0;

for (int i = 0; i &lt; arr2.length; i++) {
    for (int j = 0; j &lt; arr2.length; j++) {
        if (i != j &amp;&amp; arr2[i] - arr2[j] &gt; answer) {
            indexI = i;
            indexJ = j;
            answer = arr2[i] - arr2[j];
        }
    }
}
System.out.println(&quot;두 수의 차이가 가장 큰 쌍 : &quot;
        + arr2[indexI] + &quot; - &quot; + arr2[indexJ] + &quot; = &quot; + answer);</code></pre>
<h3 id="출력-2">출력</h3>
<pre><code>두 수의 차이가 가장 큰 쌍 : 10 - 1 = 9</code></pre><h3 id="동작-원리-2">동작 원리</h3>
<p>두 수의 차이가 가장 큰 쌍을 찾아야 하므로 2중 반복문으로 처음부터 끝까지 두 수의 차이값을 구한다. 현재 차이값이 <code>answer</code>보다 크면 <code>answer</code>를 현재값으로 갱신하고 인덱스도 저장한다.</p>
<p>이번엔 안쪽 변수 <code>j</code>의 초기값을 <code>0</code>으로 한 이유는 뺄셈의 경우 숫자 위치에 따라 값이 완전히 달라지기 때문이다. 예를 들어 <code>3 - 10</code>과 <code>10 - 3</code>은 완전히 다른 값이다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[HTML/CSS/TypeScript로 AI 채팅 화면 만들기(2) - (무료)제미나이 API 붙이기]]></title>
            <link>https://velog.io/@jhwest-dev/day-8-HTMLCSSTypeScript%EB%A1%9C-AI-%EC%B1%84%ED%8C%85-%ED%99%94%EB%A9%B4-%EB%A7%8C%EB%93%A4%EA%B8%B02</link>
            <guid>https://velog.io/@jhwest-dev/day-8-HTMLCSSTypeScript%EB%A1%9C-AI-%EC%B1%84%ED%8C%85-%ED%99%94%EB%A9%B4-%EB%A7%8C%EB%93%A4%EA%B8%B02</guid>
            <pubDate>Mon, 27 Apr 2026 08:29:13 GMT</pubDate>
            <description><![CDATA[<h1 id="js로-gemini-api-붙여보기무료">JS로 Gemini API 붙여보기(무료)</h1>
<blockquote>
<p>제미나이 API는 무료로 사용할 수 있다. 이번 포스팅에서는 별도의 프레임워크 없이 JavaScript로 채팅 화면에 Gemini API를 연동하는 방법을 정리해본다.</p>
</blockquote>
<hr>
<h2 id="1-api-키-발급">1. API 키 발급</h2>
<p><strong>Google AI Studio</strong> 에 접속해서 API 키를 발급받는다.</p>
<p>👉 <a href="https://aistudio.google.com/">https://aistudio.google.com/</a></p>
<p>접속 후 로그인 → <strong>&quot;API 키 만들기&quot;</strong> 클릭</p>
<p><img src="https://velog.velcdn.com/images/jhwest-dev/post/53459d10-b97a-4683-b1dc-69cf69dc73f4/image.png" alt="API 키 만들기"></p>
<hr>
<h2 id="2-api-키-생성-확인">2. API 키 생성 확인</h2>
<p>생성된 키를 복사해둔다.</p>
<p><img src="https://velog.velcdn.com/images/jhwest-dev/post/a2e5e6cb-5a7f-4987-926e-3c54b1cff1f1/image.png" alt="API 키 생성"></p>
<blockquote>
<p>⚠️ <strong>주의</strong>: API 키는 코드에 직접 하드코딩하지 말자.<br><code>config.js</code> 파일을 따로 만들어서 관리하고, <code>.gitignore</code>에 추가하는 것을 권장한다.</p>
</blockquote>
<pre><code class="language-js">// config.js (git과 블로그에는 올리지 않음)
const CONFIG = {
  API_KEY: &quot;여기에_본인_API_KEY_입력&quot;
};</code></pre>
<pre><code class="language-js">// main.js
const API_KEY = CONFIG.API_KEY;</code></pre>
<hr>
<h2 id="3-api-호출-코드">3. API 호출 코드</h2>
<p>아래는 채팅 화면에 Gemini API를 연동한 전체 코드다.</p>
<pre><code class="language-js">async function ask() {
  const inputElement = document.getElementById(&quot;questionInput&quot;);
  const prompt = inputElement.value;

  // 빈 입력 체크
  if (!prompt || prompt.trim() === &quot;&quot;) {
    document.getElementById(&quot;output&quot;).innerText = &quot;질문을 입력해주세요.&quot;;
    return;
  }

  // 입력창 바로 초기화
  inputElement.value = &quot;&quot;;

  // [1단계] 화면에 질문과 &quot;...(생각 중)&quot; 상태를 먼저 표시
  const chatGrid = document.getElementById(&quot;chatMessagesGrid&quot;);
  chatGrid.innerHTML = `
    &lt;article class=&#39;message-card active&#39; role=listitem&gt;
      &lt;div class=&#39;message-card-user&#39;&gt;${prompt}&lt;/div&gt;
      &lt;div class=&#39;message-card-bot&#39;&gt;
        &lt;div class=&quot;bot-logo&quot;&gt;
          👩‍💻 Assistant AI
        &lt;/div&gt;
        &lt;div class=&quot;bot-answer thinking&quot;&gt;...(생각 중)&lt;/div&gt;
      &lt;/div&gt;
    &lt;/article&gt;`;

  try {
    // [2단계] Gemini API 호출
    const res = await fetch(
      `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-lite:generateContent?key=${API_KEY}`,
      {
        method: &quot;POST&quot;,
        headers: { &quot;Content-Type&quot;: &quot;application/json&quot; },
        body: JSON.stringify({
          contents: [{ parts: [{ text: prompt }] }]
        })
      }
    );

    if (!res.ok) throw new Error(&quot;API 호출 실패&quot;);

    const data = await res.json();
    const text =
      data.candidates?.[0]?.content?.parts?.[0]?.text ??
      &quot;응답을 가져올 수 없습니다.&quot;;

    // [3단계] 자연스러운 UX를 위해 1.5초 딜레이
    await new Promise(resolve =&gt; setTimeout(resolve, 1500));

    // [4단계] &quot;...(생각 중)&quot;을 실제 답변으로 교체
    const botAnswerDiv = chatGrid.querySelector(&#39;.bot-answer&#39;);
    if (botAnswerDiv) {
      botAnswerDiv.classList.remove(&#39;thinking&#39;);
      botAnswerDiv.innerText = text;
    }

  } catch (err) {
    console.error(&quot;에러 발생:&quot;, err);
    const botAnswerDiv = chatGrid.querySelector(&#39;.bot-answer&#39;);
    if (botAnswerDiv) botAnswerDiv.innerText = &quot;오류가 발생했습니다.&quot;;
  }
}

window.ask = ask;</code></pre>
<hr>
<h2 id="4-작동-결과">4. 작동 결과</h2>
<p>질문을 입력하면 <code>...(생각 중)</code> 상태가 먼저 표시되고, 
<img src="https://velog.velcdn.com/images/jhwest-dev/post/02d5a660-30ad-4ef1-834f-17f94965d5ec/image.png" alt="">
응답이 오면 실제 답변으로 교체된다.
<img src="https://velog.velcdn.com/images/jhwest-dev/post/bb3b9080-b1a7-4a65-961d-9ea66b4382aa/image.png" alt=""></p>
<hr>
<h2 id="작동-원리">작동 원리</h2>
<pre><code>1. 질문 입력
   ↓
2. 화면에 질문 + &quot;...&quot; (로딩 상태) 즉시 표시
   ↓
3. Gemini API에 POST 요청
   ↓
4. 응답 수신 후 &quot;...&quot;을 실제 답변으로 교체</code></pre><p>응답을 받아오는 경로는 다음과 같다.</p>
<pre><code class="language-js">const text =
      data.candidates?.[0]?.content?.parts?.[0]?.text ??
      &quot;응답을 가져올 수 없습니다.&quot;;</code></pre>
<p>Gemini API는 응답을 <code>candidates</code> 배열 형태로 반환하기 때문에, 옵셔널 체이닝(<code>?.</code>)으로 안전하게 접근한다.</p>
<hr>
<h2 id="현재-한계-및-개선-포인트">현재 한계 및 개선 포인트</h2>
<table>
<thead>
<tr>
<th>항목</th>
<th>현재 상태</th>
<th>개선 방향</th>
</tr>
</thead>
<tbody><tr>
<td>대화 기록</td>
<td>❌ 저장 안 됨 (매 질문마다 리셋)</td>
<td>이전 메시지를 <code>contents</code> 배열에 누적해서 전달</td>
</tr>
<tr>
<td>마크다운 렌더링</td>
<td>❌ 텍스트 그대로 출력</td>
<td><code>marked.js</code> 등의 라이브러리로 렌더링</td>
</tr>
<tr>
<td>스트리밍</td>
<td>❌ 응답을 한 번에 받아옴</td>
<td><code>streamGenerateContent</code> 엔드포인트 + <code>ReadableStream</code>으로 처리</td>
</tr>
<tr>
<td>API 키 보안</td>
<td>⚠️ 브라우저에서 노출됨</td>
<td>실서비스에서는 백엔드 서버를 통해 호출</td>
</tr>
</tbody></table>
<p>현재는 응답이 완전히 생성된 후에 한 번에 화면에 출력된다. 스트리밍을 적용하면 ChatGPT처럼 글자가 순차적으로 타이핑되는 효과를 구현할 수 있다</p>
<hr>
<h2 id="마무리">마무리</h2>
<p>별도의 프레임워크나 백엔드 서버 없이도 Gemini API를 빠르게 연동할 수 있었다.
다음 단계로는 대화 기록 누적, 마크다운 렌더링, 그리고 스트리밍 방식으로의 전환을 시도해볼 예정이다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Java] 정렬 알고리즘 - 선택 정렬(Selection Sort)]]></title>
            <link>https://velog.io/@jhwest-dev/Java-%EC%A0%95%EB%A0%AC-%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98-%EC%84%A0%ED%83%9D-%EC%A0%95%EB%A0%AC</link>
            <guid>https://velog.io/@jhwest-dev/Java-%EC%A0%95%EB%A0%AC-%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98-%EC%84%A0%ED%83%9D-%EC%A0%95%EB%A0%AC</guid>
            <pubDate>Mon, 27 Apr 2026 07:07:18 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>매 라운드에서 <strong>최솟값을 선택</strong>해 앞으로 보내는 방식의 정렬 알고리즘이다.</p>
</blockquote>
<hr>
<h2 id="1-선택-정렬을-적용할-배열-생성-및-출력">1. 선택 정렬을 적용할 배열 생성 및 출력</h2>
<pre><code class="language-java">import java.util.Arrays;

public class SelectionSort {
    public static void main(String[] args) {
        int[] arr = {3, 8, 4, 9, 2, 1};
        System.out.println(&quot;정렬 전 : &quot; + Arrays.toString(arr));
    }
}</code></pre>
<blockquote>
<p>정렬 전 : [3, 8, 4, 9, 2, 1]</p>
</blockquote>
<hr>
<h2 id="2-선택-정렬-메서드-정의">2. 선택 정렬 메서드 정의</h2>
<pre><code class="language-java">import java.util.Arrays;

public class SelectionSort {
    public static void main(String[] args) {
        int[] arr = {3, 8, 4, 9, 2, 1};
        System.out.println(&quot;정렬 전 : &quot; + Arrays.toString(arr));
        selectionSortVisual(arr);
        System.out.println(&quot;정렬 후 : &quot; + Arrays.toString(arr));
    }

    // 선택 정렬 메서드
    static void selectionSortVisual(int[] arr) {
        int n = arr.length;

        for (int i = 0; i &lt; n - 1; i++) {
            // i번째 위치부터 끝까지 최솟값의 인덱스를 찾음
            int minIndex = i; // 현재 최솟값의 인덱스
            System.out.println((i + 1) + &quot;번째 인덱스 &quot; + i + &quot; ~ &quot; + (n - 1) + &quot;에서 최솟값 선택&quot;);

            for (int j = i + 1; j &lt; n; j++) {
                System.out.printf(&quot;arr[%d] = %d vs 현재 최솟값[%d] = %d -&gt; &quot;, j, arr[j], minIndex, arr[minIndex]);
                if (arr[j] &lt; arr[minIndex]) {
                    minIndex = j;
                    System.out.println(&quot;최솟값 갱신 ! 새 최솟값 = &quot; + arr[minIndex]);
                } else {
                    System.out.println(&quot;유지&quot;);
                }
            }

            // i번째와 최솟값 위치 교환
            if (minIndex != i) {
                System.out.printf(&quot;교환 : arr[%d] = %d &lt;-&gt; arr[%d] = %d%n&quot;, i, arr[i], minIndex, arr[minIndex]);
                int temp = arr[i];
                arr[i] = arr[minIndex];
                arr[minIndex] = temp;
            } else {
                System.out.println(&quot;교환 없음&quot;);
            }

            System.out.println((i + 1) + &quot;번째 완료 &quot; + Arrays.toString(arr));
            System.out.println();
        }
    }
}</code></pre>
<hr>
<h2 id="3-동작-원리-설명">3. 동작 원리 설명</h2>
<p><strong>① minIndex 초기화</strong></p>
<p>바깥쪽 루프에서 <code>minIndex = i</code>로 초기화한다.<br>여기서 <code>minIndex</code>는 값이 아닌 <strong>인덱스(위치)</strong>를 저장하는 변수이므로, <code>arr[i]</code>가 아닌 <code>i</code>로 초기화한다는 점에 주의한다.<br>즉, 현재 <code>i</code> 위치를 일단 최솟값의 위치라고 가정하고 시작한다.</p>
<p><strong>② 안쪽 루프에서 최솟값 탐색</strong></p>
<p><code>j</code>는 <code>i + 1</code>부터 시작해서 배열 끝까지 순회한다.<br><code>arr[j]</code>와 <code>arr[minIndex]</code>를 비교해 <code>arr[j]</code>가 더 작으면 <code>minIndex = j</code>로 갱신한다.<br>안쪽 루프가 끝나면 <code>minIndex</code>에는 <code>i</code>번째 이후 구간에서의 최솟값 인덱스가 담겨 있다.</p>
<p><strong>③ 교환 (Swap)</strong></p>
<p>안쪽 루프 종료 후, <code>minIndex != i</code>이면 <code>arr[i]</code>와 <code>arr[minIndex]</code>를 교환한다.<br>이미 최솟값이 제자리(<code>minIndex == i</code>)라면 교환하지 않는다.</p>
<p><strong>④ 반복</strong></p>
<p>위 과정을 <code>n - 1</code>번 반복하면 배열 전체가 오름차순으로 정렬된다.</p>
<hr>
<h2 id="4-실행-결과">4. 실행 결과</h2>
<pre><code>정렬 전 : [3, 8, 4, 9, 2, 1]

1번째 인덱스 0 ~ 5에서 최솟값 선택
arr[1] = 8 vs 현재 최솟값[0] = 3 -&gt; 유지
arr[2] = 4 vs 현재 최솟값[0] = 3 -&gt; 유지
arr[3] = 9 vs 현재 최솟값[0] = 3 -&gt; 유지
arr[4] = 2 vs 현재 최솟값[0] = 3 -&gt; 최솟값 갱신 ! 새 최솟값 = 2
arr[5] = 1 vs 현재 최솟값[4] = 2 -&gt; 최솟값 갱신 ! 새 최솟값 = 1
교환 : arr[0] = 3 &lt;-&gt; arr[5] = 1
1번째 완료 [1, 8, 4, 9, 2, 3]

2번째 인덱스 1 ~ 5에서 최솟값 선택
arr[2] = 4 vs 현재 최솟값[1] = 8 -&gt; 최솟값 갱신 ! 새 최솟값 = 4
arr[3] = 9 vs 현재 최솟값[2] = 4 -&gt; 유지
arr[4] = 2 vs 현재 최솟값[2] = 4 -&gt; 최솟값 갱신 ! 새 최솟값 = 2
arr[5] = 3 vs 현재 최솟값[4] = 2 -&gt; 유지
교환 : arr[1] = 8 &lt;-&gt; arr[4] = 2
2번째 완료 [1, 2, 4, 9, 8, 3]

...

정렬 후 : [1, 2, 3, 4, 8, 9]</code></pre><hr>
<h2 id="5-시간-복잡도">5. 시간 복잡도</h2>
<table>
<thead>
<tr>
<th>케이스</th>
<th>시간 복잡도</th>
</tr>
</thead>
<tbody><tr>
<td>최선</td>
<td>O(n²)</td>
</tr>
<tr>
<td>평균</td>
<td>O(n²)</td>
</tr>
<tr>
<td>최악</td>
<td>O(n²)</td>
</tr>
</tbody></table>
<ul>
<li><strong>비교 횟수</strong> : n²/2번 → 바깥 루프 n번 × 안쪽 루프 평균 n/2번</li>
<li><strong>교환 횟수</strong> : 최대 n-1번 → 라운드마다 딱 1번만 교환</li>
</ul>
<p>선택 정렬은 최선의 경우에도 항상 이중 루프를 돌기 때문에 O(n²)이다.<br>데이터 개수가 많을수록 성능이 급격히 떨어지므로, 소규모 데이터나 학습용으로 적합한 알고리즘이다.</p>
<hr>
<h2 id="6-공간-복잡도">6. 공간 복잡도</h2>
<table>
<thead>
<tr>
<th>공간 복잡도</th>
</tr>
</thead>
<tbody><tr>
<td>O(1)</td>
</tr>
</tbody></table>
<p><code>temp</code>, <code>minIndex</code> 같은 변수 몇 개만 사용하고 추가 배열을 만들지 않기 때문에 메모리를 거의 사용하지 않는다.</p>
<hr>
<h2 id="7-특징">7. 특징</h2>
<p><strong>① 교환 횟수가 적다</strong></p>
<p>라운드당 최대 1번만 교환하므로, 교환 비용이 비싼 상황(예: 디스크 I/O, 큰 객체 이동)에서 버블 정렬보다 유리하다.</p>
<p><strong>② 이미 정렬된 배열도 O(n²)</strong></p>
<p>최솟값을 찾는 비교 자체는 항상 수행되기 때문에 <strong>최적화가 불가능</strong>하다.</p>
<p><strong>③ 불안정 정렬 (Unstable Sort)</strong></p>
<p>같은 값의 상대적 순서가 바뀔 수 있다.<br>예를 들어 <code>[3a, 3b, 1]</code>을 정렬하면 3a와 3b의 순서가 뒤바뀔 수 있다.<br>안정 정렬이 필요한 경우에는 삽입 정렬이나 병합 정렬을 사용해야 한다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Java] 재귀함수 이해하기]]></title>
            <link>https://velog.io/@jhwest-dev/Java-%EC%9E%AC%EA%B7%80%ED%95%A8%EC%88%98-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@jhwest-dev/Java-%EC%9E%AC%EA%B7%80%ED%95%A8%EC%88%98-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0</guid>
            <pubDate>Thu, 23 Apr 2026 16:16:55 GMT</pubDate>
            <description><![CDATA[<h2 id="재귀-함수란">재귀 함수란?</h2>
<blockquote>
<p><strong>함수가 자기 자신을 호출하는 것</strong></p>
</blockquote>
<ul>
<li>장점 : 이해가 선행된다면 코드가 직관적이고 읽기 쉬운 경우가 있음</li>
<li>단점 : 함수 호출마다 스택 메모리 사용 -&gt; 깊어지면 StackOverFlowError</li>
</ul>
<hr>
<h2 id="재귀의-두-가지-필수-요소">재귀의 두 가지 필수 요소</h2>
<table>
<thead>
<tr>
<th>요소</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><strong>1) 기저 조건 (Base Case)</strong></td>
<td>재귀를 멈추는 조건. 없거나 잘못되면 무한 루프 발생 (StackOverFlowError)⚠️</td>
</tr>
<tr>
<td><strong>2) 재귀 호출 (Recursive Call)</strong></td>
<td>자기 자신을 더 작은 문제로 호출</td>
</tr>
</tbody></table>
<hr>
<h2 id="📌-예제-1--팩토리얼-구하기-5">📌 예제 1 : 팩토리얼 구하기 (5!)</h2>
<blockquote>
<p>5! = 5 × 4 × 3 × 2 × 1 = <strong>120</strong></p>
</blockquote>
<h3 id="코드">코드</h3>
<pre><code class="language-java">static long factorial(int n) {
    if (n == 0) {   // 기저 조건 : 0! = 1
        return 1;
    }
    return n * factorial(n - 1);  // 재귀 호출
}</code></pre>
<hr>
<h3 id="🔍-동작-방식--factorial5-호출-시">🔍 동작 방식 — <code>factorial(5)</code> 호출 시</h3>
<p>재귀 함수는 크게 두 단계로 나뉜다.</p>
<h4 id="1단계--호출-스택이-쌓이는-과정-↓-내려가기">1단계 : 호출 스택이 쌓이는 과정 (↓ 내려가기)</h4>
<pre><code>factorial(5)
  └─ 5 * factorial(4)
         └─ 4 * factorial(3)
                └─ 3 * factorial(2)
                       └─ 2 * factorial(1)
                              └─ 1 * factorial(0)
                                     └─ n == 0 → return 1  🛑 기저 조건 도달!</code></pre><h4 id="2단계--값이-반환되는-과정-↑-올라오기">2단계 : 값이 반환되는 과정 (↑ 올라오기)</h4>
<pre><code>return 1
return 1 * 1  = 1
return 2 * 1  = 2
return 3 * 2  = 6
return 4 * 6  = 24
return 5 * 24 = 120  🎯</code></pre><p>정답 : <code>120</code></p>
<hr>
<h2 id="📌-예제-2--피보나치-수열-구하기">📌 예제 2 : 피보나치 수열 구하기</h2>
<p>피보나치 수열 : 1, 1, 2, 3, 5, 8, 13, 21, 34 ...<br>규칙 : 앞의 두 수를 더하면 다음 수가 된다.</p>
<h3 id="코드-1">코드</h3>
<pre><code class="language-java">static int fibRecursive(int n) {
    // 기저 조건
    if (n == 1 || n == 2) {
        return 1; // n이 1이거나 2면 무조건 1 반환, 재귀 실행 x
    }
    // 재귀 호출
    return fibRecursive(n - 1) + fibRecursive(n - 2);
}</code></pre>
<hr>
<h3 id="🔍-동작-방식--fibrecursive4-호출-시">🔍 동작 방식 — <code>fibRecursive(4)</code> 호출 시</h3>
<h4 id="반환값은">반환값은?</h4>
<p>피보나치 수열의 4번째 값 = <strong>3</strong> (1, 1, 2, <strong>3</strong>, 5 ...)</p>
<h4 id="어떻게-그-값이-나오는가">어떻게 그 값이 나오는가?</h4>
<p>재귀 함수는 <strong>트리 아래로 내려가며 쪼개고, 위로 올라오며 합산</strong>한다.</p>
<pre><code>fib(1) = 1   ← 기저조건 (바로 반환)
fib(2) = 1   ← 기저조건 (바로 반환)

fib(3) = fib(2) + fib(1) = 1 + 1 = 2
fib(4) = fib(3) + fib(2) = 2 + 1 = 3</code></pre><h4 id="1단계--호출-스택이-쌓이는-과정-↓-내려가기-1">1단계 : 호출 스택이 쌓이는 과정 (↓ 내려가기)</h4>
<pre><code>fibRecursive(4)
  └─ fibRecursive(3) + fibRecursive(2)
         └─ fibRecursive(2) + fibRecursive(1)
                └─ n==1 또는 n==2 → return 1  🛑 기저 조건 도달!</code></pre><h4 id="2단계--값이-반환되는-과정-↑-올라오기-1">2단계 : 값이 반환되는 과정 (↑ 올라오기)</h4>
<pre><code>fib(1) = 1
fib(2) = 1
fib(3) = fib(2) + fib(1) = 1 + 1 = 2
fib(4) = fib(3) + fib(2) = 2 + 1 = 3  🎯</code></pre><h4 id="호출-트리-시각화">호출 트리 시각화</h4>
<p><img src="https://velog.velcdn.com/images/jhwest-dev/post/021bd9c3-23d9-429c-9ca8-bf7be89aaf70/image.png" alt=""></p>
<hr>
<h2 id="📌-예제-3--하노이의-탑">📌 예제 3 : 하노이의 탑</h2>
<h3 id="문제-설명">문제 설명</h3>
<p>기둥이 세 개 있음  — <strong>A, B, C</strong></p>
<ul>
<li>기둥 A에는 크기가 다른 원판이 <strong>n개</strong> 쌓여 있다.</li>
<li>모든 원판을 <strong>기둥 C로</strong> 옮겨야 한다.</li>
</ul>
<p><strong>규칙은 딱 두 가지:</strong></p>
<ol>
<li>한 번에 <strong>하나의 원판</strong>만 이동할 수 있다.</li>
<li><strong>작은 원판 위에 큰 원판</strong>을 올릴 수 없다.</li>
</ol>
<blockquote>
<p>💡 <strong>목표:</strong> 원판이 n개일 때 최소 이동 횟수를 구하라!</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/jhwest-dev/post/46247fdf-f9de-4446-bb03-2d98a44c4750/image.png" alt=""></p>
<hr>
<h3 id="🔢-예시">🔢 예시</h3>
<h3 id="원판-1개-→-이동-횟수-1">원판 1개 → 이동 횟수: 1</h3>
<pre><code>A → C  (끝!)</code></pre><p>바로 C로 옮기면 됨. <strong>기저 조건(Base Case)</strong> 에 해당</p>
<h3 id="원판-2개-→-이동-횟수-3">원판 2개 → 이동 횟수: 3</h3>
<pre><code>1. 작은 원판: A → B
2. 큰 원판:   A → C
3. 작은 원판: B → C</code></pre><h3 id="원판-3개-→-이동-횟수-7">원판 3개 → 이동 횟수: 7</h3>
<pre><code>1. 제일 작은 원판: A → C
2. 중간 원판:      A → B
3. 제일 작은 원판: C → B
4. 제일 큰 원판:   A → C  ← 핵심 이동!
5. 제일 작은 원판: B → A
6. 중간 원판:      B → C
7. 제일 작은 원판: A → C</code></pre><p>크게 세 덩어리로 보면:</p>
<table>
<thead>
<tr>
<th>단계</th>
<th>설명</th>
<th>이동 횟수</th>
</tr>
</thead>
<tbody><tr>
<td>1단계</td>
<td>위 2개를 임시 기둥(B)으로 이동</td>
<td>3</td>
</tr>
<tr>
<td>2단계</td>
<td>제일 큰 원판을 C로 이동</td>
<td>1</td>
</tr>
<tr>
<td>3단계</td>
<td>임시 기둥(B)의 2개를 C로 이동</td>
<td>3</td>
</tr>
<tr>
<td><strong>합계</strong></td>
<td></td>
<td><strong>7</strong></td>
</tr>
</tbody></table>
<hr>
<h2 id="💻-코드-구현">💻 코드 구현</h2>
<pre><code class="language-java">static int hanoiCount = 0; // 이동 횟수 카운터

static void hanoi(int n, char from, char to, char temp) {
    // 기저 조건: 원판이 1개면 바로 이동
    if (n == 1) {
        System.out.println(&quot;원판&quot; + n + &quot; 이동: &quot; + from + &quot; → &quot; + to);
        hanoiCount++;
        return;
    }

    // 1단계: 위의 (n-1)개를 from → temp (to를 임시 공간으로)
    hanoi(n - 1, from, temp, to);

    // 2단계: 가장 큰 원판을 from → to
    System.out.println(&quot;원판&quot; + n + &quot; 이동: &quot; + from + &quot; → &quot; + to);
    hanoiCount++;

    // 3단계: temp의 (n-1)개를 temp → to (from을 임시 공간으로)
    hanoi(n - 1, temp, to, from);
}</code></pre>
<hr>
<h2 id="📊-count-발생-위치-정리">📊 count++ 발생 위치 정리</h2>
<pre><code>기저 조건 count++  →  4번  (n=1인 hanoi 4개)
n=2 중간 count++   →  2번  (hanoi(2) 두 번의 2단계)
n=3 중간 count++   →  1번  (hanoi(3)의 2단계)
─────────────────────────
합계               →  7번</code></pre><p><img src="https://velog.velcdn.com/images/jhwest-dev/post/f2ff7b70-d35d-4e98-afe8-85dcb0b5778a/image.png" alt=""></p>
<hr>
<blockquote>
<p>💬 <strong>마치며</strong>
재귀는 개념 자체는 단순하지만, 막상 코드를 보면 흐름을 머릿속으로 따라가기가 쉽지 않았다. 특히 하노이의 탑처럼 재귀가 여러 겹으로 쌓이는 경우엔 더 그랬다.
짝꿍이 알려준 방법대로 직접 그려보며 이해해 보려 했다. 그게 생각보다 훨씬 효과적이었다.
재귀는 익숙해질수록 점점 더 자연스럽게 읽히는 것 같다. 앞으로 문제를 더 풀면서 감각을 키워볼 생각이다.</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Java] 컬렉션 프레임워크(ArrayList · HashSet · TreeSet · HashMap · TreeMap) 정리]]></title>
            <link>https://velog.io/@jhwest-dev/Java-%EC%BB%AC%EB%A0%89%EC%85%98-%ED%94%84%EB%A0%88%EC%9E%84%EC%9B%8C%ED%81%AC-%ED%95%B5%EC%8B%AC-%EC%A0%95%EB%A6%AC-ArrayList-Set-Map</link>
            <guid>https://velog.io/@jhwest-dev/Java-%EC%BB%AC%EB%A0%89%EC%85%98-%ED%94%84%EB%A0%88%EC%9E%84%EC%9B%8C%ED%81%AC-%ED%95%B5%EC%8B%AC-%EC%A0%95%EB%A6%AC-ArrayList-Set-Map</guid>
            <pubDate>Wed, 22 Apr 2026 15:05:12 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p><code>ArrayList</code> · <code>HashSet</code> · <code>TreeSet</code> · <code>HashMap</code> · <code>TreeMap</code></p>
</blockquote>
<p>오늘 자바의 자료구조인 컬렉션 프레임워크(ArrayList, HashSet, TreeSet, HashMap, TreeMap)를 공부했다. 수많은 메서드가 있지만, 가장 많이 쓰이는 위주로 정리해 보았다.</p>
<hr>
<h2 id="📁-컬렉션-정리">📁 컬렉션 정리</h2>
<br>

<table>
<thead>
<tr>
<th>구분</th>
<th>구현 클래스</th>
<th>특징</th>
<th>중복 허용</th>
<th>정렬/순서</th>
</tr>
</thead>
<tbody><tr>
<td>List</td>
<td>ArrayList</td>
<td>인덱스로 관리하는 동적 배열</td>
<td>✅</td>
<td>입력 순서 유지</td>
</tr>
<tr>
<td>Set</td>
<td>HashSet</td>
<td>집합 개념, 빠른 검색 속도</td>
<td>❌</td>
<td>❌</td>
</tr>
<tr>
<td>Set</td>
<td>TreeSet</td>
<td>이진 트리 구조의 집합</td>
<td>❌</td>
<td>값 기준 정렬</td>
</tr>
<tr>
<td>Map</td>
<td>HashMap</td>
<td>Key-Value 쌍으로 저장</td>
<td>Key ❌</td>
<td>❌</td>
</tr>
<tr>
<td>Map</td>
<td>TreeMap</td>
<td>Key-Value 쌍 + 자동 정렬</td>
<td>Key ❌</td>
<td>키 기준 정렬</td>
</tr>
<tr>
<td><br></td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
</tbody></table>
<hr>
<h2 id="🔍-메서드-정리">🔍 메서드 정리</h2>
<h3 id="list--set-공통-메서드">List &amp; Set 공통 메서드</h3>
<table>
<thead>
<tr>
<th>메서드</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><code>add(E e)</code></td>
<td>요소 추가</td>
</tr>
<tr>
<td><code>remove(Object o)</code></td>
<td>요소 삭제</td>
</tr>
<tr>
<td><code>contains(Object o)</code></td>
<td>포함 여부 확인 (있으면 <code>true</code>)</td>
</tr>
<tr>
<td><code>size()</code></td>
<td>요소 개수 반환</td>
</tr>
<tr>
<td><code>clear()</code></td>
<td>모든 요소 삭제</td>
</tr>
<tr>
<td><code>isEmpty()</code></td>
<td>비어있으면 <code>true</code></td>
</tr>
<tr>
<td><br></td>
<td></td>
</tr>
</tbody></table>
<pre><code class="language-java">List&lt;String&gt; list = new ArrayList&lt;&gt;();
list.add(&quot;Apple&quot;);        // [Apple]
list.add(&quot;Banana&quot;);       // [Apple, Banana]
list.remove(&quot;Apple&quot;);     // [Banana]
list.contains(&quot;Banana&quot;);  // true
list.size();              // 1
list.isEmpty();           // false
list.clear();             // []

Set&lt;String&gt; set = new HashSet&lt;&gt;();
set.add(&quot;Apple&quot;);
set.add(&quot;Apple&quot;);  // 중복 → 무시됨
set.size();        // 1</code></pre>
<hr>
<h3 id="list-전용-메서드">List 전용 메서드</h3>
<blockquote>
<p>Set은 순서가 없어 인덱스 개념이 없으므로 아래 메서드는 <strong>List에서만</strong> 사용할 수 있다.</p>
</blockquote>
<table>
<thead>
<tr>
<th>메서드</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><code>get(int index)</code></td>
<td>해당 인덱스의 요소 반환</td>
</tr>
<tr>
<td><code>set(int index, E e)</code></td>
<td>해당 인덱스의 요소를 교체</td>
</tr>
<tr>
<td><code>indexOf(Object o)</code></td>
<td>요소의 첫 번째 인덱스 반환 (없으면 <code>-1</code>)</td>
</tr>
<tr>
<td><br></td>
<td></td>
</tr>
</tbody></table>
<pre><code class="language-java">List&lt;String&gt; list = new ArrayList&lt;&gt;();
list.add(&quot;Apple&quot;);
list.add(&quot;Banana&quot;);
list.add(&quot;Cherry&quot;);

list.get(0);               // &quot;Apple&quot;
list.set(1, &quot;Blueberry&quot;);  // [Apple, Blueberry, Cherry]
list.indexOf(&quot;Cherry&quot;);    // 2
list.indexOf(&quot;Mango&quot;);     // -1 (없는 요소)</code></pre>
<hr>
<h3 id="map-메서드">Map 메서드</h3>
<p>키와 값을 쌍으로 저장하는 구조.</p>
<table>
<thead>
<tr>
<th>메서드</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><code>put(K key, V value)</code></td>
<td>Key-Value 추가 (키 중복 시 값 덮어쓰기)</td>
</tr>
<tr>
<td><code>get(Object key)</code></td>
<td>키에 해당하는 값 반환 (없으면 <code>null</code>)</td>
</tr>
<tr>
<td><code>remove(Object key)</code></td>
<td>키에 해당하는 쌍 삭제</td>
</tr>
<tr>
<td><code>containsKey(Object key)</code></td>
<td>키 존재 여부 확인</td>
</tr>
<tr>
<td><code>keySet()</code></td>
<td>모든 키를 <code>Set</code>으로 반환</td>
</tr>
<tr>
<td><code>values()</code></td>
<td>모든 값을 <code>Collection</code>으로 반환</td>
</tr>
</tbody></table>
<br>

<pre><code class="language-java">Map&lt;String, Integer&gt; map = new HashMap&lt;&gt;();
map.put(&quot;홍길동&quot;, 90);
map.put(&quot;이순신&quot;, 85);
map.put(&quot;홍길동&quot;, 95);  // 키 중복 → 값 95로 덮어쓰기

map.get(&quot;홍길동&quot;);         // 95
map.get(&quot;강감찬&quot;);         // null (없는 키)
map.containsKey(&quot;이순신&quot;); // true
map.keySet();              // [홍길동, 이순신]
map.values();              // [95, 85]
map.remove(&quot;이순신&quot;);      // 이순신 쌍 삭제

// keySet()으로 전체 순회
for (String key : map.keySet()) {
    System.out.println(key + &quot; : &quot; + map.get(key));
}</code></pre>
<hr>
<h3 id="tree-계열-전용-메서드-treeset--treemap">Tree 계열 전용 메서드 (TreeSet / TreeMap)</h3>
<p>정렬이 보장되기 때문에 <strong>범위 검색</strong>과 <strong>최솟값/최댓값 조회</strong>가 가능.</p>
<table>
<thead>
<tr>
<th>메서드</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><code>first()</code> / <code>firstKey()</code></td>
<td>가장 작은 값(키) 반환</td>
</tr>
<tr>
<td><code>last()</code> / <code>lastKey()</code></td>
<td>가장 큰 값(키) 반환</td>
</tr>
<tr>
<td><code>subSet(from, to)</code> / <code>subMap(from, to)</code></td>
<td>from 포함, to 미포함</td>
</tr>
<tr>
<td><code>headSet(to)</code> / <code>headMap(to)</code></td>
<td><code>to</code> <strong>미만</strong>의 요소 반환</td>
</tr>
<tr>
<td><code>tailSet(from)</code> / <code>tailMap(from)</code></td>
<td><code>from</code> <strong>이상</strong>의 요소 반환</td>
</tr>
</tbody></table>
<pre><code class="language-java">// TreeSet 예제
TreeSet&lt;Integer&gt; treeSet = new TreeSet&lt;&gt;();
treeSet.add(30); treeSet.add(10); treeSet.add(50); treeSet.add(20);
// 자동 정렬됨 → [10, 20, 30, 50]

treeSet.first();         // 10 (최솟값)
treeSet.last();          // 50 (최댓값)
treeSet.subSet(10, 40);  // [10, 20, 30] (40 미포함)
treeSet.headSet(30);     // [10, 20] (30 미포함)
treeSet.tailSet(20);     // [20, 30, 50]

// TreeMap 예제
TreeMap&lt;String, Integer&gt; treeMap = new TreeMap&lt;&gt;();
treeMap.put(&quot;banana&quot;, 2); treeMap.put(&quot;apple&quot;, 5); treeMap.put(&quot;cherry&quot;, 1);
// 키 기준 알파벳 정렬 → {apple=5, banana=2, cherry=1}

treeMap.firstKey();                 // &quot;apple&quot;
treeMap.lastKey();                  // &quot;cherry&quot;
treeMap.subMap(&quot;apple&quot;, &quot;cherry&quot;); // {apple=5, banana=2}</code></pre>
<hr>
<h2 id="⚡-성능-비교--hash-vs-tree">⚡ 성능 비교  Hash vs Tree</h2>
<p><code>HashSet</code> / <code>HashMap</code>은 해시 함수로 데이터 위치를 바로 찾아가기 때문에 <strong>O(1)</strong> 로 매우 빠르다.<br>반면 <code>TreeSet</code> / <code>TreeMap</code>은 데이터를 넣을 때마다 기존 데이터들과 비교해 정렬된 위치를 탐색하므로 <strong>O(log n)</strong> 의 비용이 발생한다.</p>
<table>
<thead>
<tr>
<th>컬렉션</th>
<th>검색/삽입/삭제</th>
<th>정렬</th>
<th>사용 기준</th>
</tr>
</thead>
<tbody><tr>
<td>HashSet / HashMap</td>
<td>O(1)</td>
<td>❌</td>
<td>기본 선택 — 속도 우선</td>
</tr>
<tr>
<td>TreeSet / TreeMap</td>
<td>O(log n)</td>
<td>✅</td>
<td>정렬이 꼭 필요할 때</td>
</tr>
</tbody></table>
<blockquote>
<p>정렬이 꼭 필요한 경우가 아니라면, <strong>Hash 계열을 사용하는 것이 기본 !</strong></p>
</blockquote>
<hr>
<h2 id="🔢-집합-연산-set">🔢 집합 연산 (Set)</h2>
<p><code>Set</code>은 수학의 집합과 동일한 개념이라 합집합, 교집합, 차집합 연산을 메서드 하나로 처리할 수 있다.</p>
<table>
<thead>
<tr>
<th>연산</th>
<th>메서드</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>합집합 (A ∪ B)</td>
<td><code>addAll(Collection c)</code></td>
<td>다른 컬렉션의 요소를 모두 추가</td>
</tr>
<tr>
<td>교집합 (A ∩ B)</td>
<td><code>retainAll(Collection c)</code></td>
<td>다른 컬렉션과 공통된 요소만 남김</td>
</tr>
<tr>
<td>차집합 (A - B)</td>
<td><code>removeAll(Collection c)</code></td>
<td>다른 컬렉션에 있는 요소를 모두 제거</td>
</tr>
</tbody></table>
<blockquote>
<p>⚠️ 이 메서드들은 <strong>원본 Set을 직접 수정</strong>하므로 원본을 보존하려면 새로운 Set에 복사한 뒤 사용.</p>
</blockquote>
<pre><code class="language-java">Set&lt;Integer&gt; A = new HashSet&lt;&gt;(Arrays.asList(1, 2, 3, 4, 5));
Set&lt;Integer&gt; B = new HashSet&lt;&gt;(Arrays.asList(3, 4, 5, 6, 7));

// 합집합 (A ∪ B) → [1, 2, 3, 4, 5, 6, 7]
Set&lt;Integer&gt; union = new HashSet&lt;&gt;(A);
union.addAll(B);
System.out.println(union);

// 교집합 (A ∩ B) → [3, 4, 5]
Set&lt;Integer&gt; intersection = new HashSet&lt;&gt;(A);
intersection.retainAll(B);
System.out.println(intersection);

// 차집합 (A - B) → [1, 2]
Set&lt;Integer&gt; difference = new HashSet&lt;&gt;(A);
difference.removeAll(B);
System.out.println(difference);</code></pre>
<h3 id="결과를-정렬된-상태로-보고-싶다면-→-treeset으로-변환">결과를 정렬된 상태로 보고 싶다면 → TreeSet으로 변환</h3>
<pre><code class="language-java">Set&lt;Integer&gt; A = new HashSet&lt;&gt;(Arrays.asList(5, 3, 1, 4, 2));
Set&lt;Integer&gt; B = new HashSet&lt;&gt;(Arrays.asList(3, 4, 5, 6, 7));

Set&lt;Integer&gt; union = new HashSet&lt;&gt;(A);
union.addAll(B);

System.out.println(union);               // HashSet → 순서 무작위
System.out.println(new TreeSet&lt;&gt;(union)); // TreeSet → 자동 정렬: [1, 2, 3, 4, 5, 6, 7]</code></pre>
<br>

<hr>
<h2 id="😎-언제-어떤-컬렉션을-쓸까">😎 언제 어떤 컬렉션을 쓸까?</h2>
<h3 id="순서가-중요할-때-→-list">순서가 중요할 때 → List</h3>
<pre><code class="language-java">List&lt;String&gt; playlist = new ArrayList&lt;&gt;();
playlist.add(&quot;노래 A&quot;);
playlist.add(&quot;노래 B&quot;);
playlist.add(&quot;노래 A&quot;);  // 중복 OK
System.out.println(playlist.get(0));  // 노래 A</code></pre>
<h3 id="중복-제거가-필요할-때-→-set">중복 제거가 필요할 때 → Set</h3>
<pre><code class="language-java">Set&lt;String&gt; visited = new HashSet&lt;&gt;();
visited.add(&quot;Seoul&quot;);
visited.add(&quot;Tokyo&quot;);
visited.add(&quot;Seoul&quot;);  // 중복 → 무시됨
System.out.println(visited.size());  // 2</code></pre>
<h3 id="관계key-value를-저장할-때-→-map">관계(Key-Value)를 저장할 때 → Map</h3>
<pre><code class="language-java">Map&lt;String, Integer&gt; score = new HashMap&lt;&gt;();
score.put(&quot;홍길동&quot;, 90);
score.put(&quot;이순신&quot;, 85);
System.out.println(score.get(&quot;홍길동&quot;));  // 90</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Java] 자바 접근제어자 정리 (private, default, protected, public)]]></title>
            <link>https://velog.io/@jhwest-dev/Java-%EC%9E%90%EB%B0%94-%EC%A0%91%EA%B7%BC%EC%A0%9C%EC%96%B4%EC%9E%90-%EC%A0%95%EB%A6%AC-private-default-protected-public</link>
            <guid>https://velog.io/@jhwest-dev/Java-%EC%9E%90%EB%B0%94-%EC%A0%91%EA%B7%BC%EC%A0%9C%EC%96%B4%EC%9E%90-%EC%A0%95%EB%A6%AC-private-default-protected-public</guid>
            <pubDate>Tue, 21 Apr 2026 08:50:43 GMT</pubDate>
            <description><![CDATA[<p>오늘은 자바에서 가장 많이 헷갈렸던 <strong>접근제어자</strong>를 정리해 보려고 한다.<br>접근 제어자는 클래스, 메서드, 필드에 대한 접근 범위를 제한하는 키워드다.
캡슐화(Encapsulation)를 구현하는 핵심 도구이며, 외부로부터 내부 구현을 보호한다.</p>
<hr>
<h2 id="💊-접근-제어자-종류-및-범위">💊 접근 제어자 종류 및 범위</h2>
<table>
<thead>
<tr>
<th align="center">접근 제어자</th>
<th align="center">같은 클래스</th>
<th align="center">같은 패키지</th>
<th align="center">자식 클래스 (다른 패키지)</th>
<th align="center">전체</th>
</tr>
</thead>
<tbody><tr>
<td align="center"><code>private</code></td>
<td align="center">✅</td>
<td align="center">❌</td>
<td align="center">❌</td>
<td align="center">❌</td>
</tr>
<tr>
<td align="center"><code>default</code></td>
<td align="center">✅</td>
<td align="center">✅</td>
<td align="center">❌</td>
<td align="center">❌</td>
</tr>
<tr>
<td align="center"><code>protected</code></td>
<td align="center">✅</td>
<td align="center">✅</td>
<td align="center">✅</td>
<td align="center">❌</td>
</tr>
<tr>
<td align="center"><code>public</code></td>
<td align="center">✅</td>
<td align="center">✅</td>
<td align="center">✅</td>
<td align="center">✅</td>
</tr>
</tbody></table>
<blockquote>
<p><strong>접근 범위</strong>: <code>private</code> &lt; <code>default</code> &lt; <code>protected</code> &lt; <code>public</code></p>
</blockquote>
<hr>
<h2 id="🔒-private">🔒 private</h2>
<h3 id="특징">특징</h3>
<ul>
<li><strong>같은 클래스 내부에서만</strong> 접근 가능</li>
<li>외부에서 직접 접근 불가 → <code>getter</code> / <code>setter</code>로 간접 접근</li>
<li>캡슐화의 핵심, 가장 강력한 접근 제한<h3 id="예시-코드">예시 코드</h3>
</li>
</ul>
<pre><code class="language-java">public class AccessModifier {
public class AccessModifier {
    public static void main(String[] args) {
        Person p = new Person();
        // p.name = &quot;홍길동&quot;;  ❌ 컴파일 에러!
        System.out.println(p.setName(&quot;&quot;)); // ✅ setter로 접근
        System.out.println(p.getName()); // ✅ getter로 접근

    }
}

class Person {
    private String name;  // 외부에서 직접 접근 불가

    // getter로 간접 접근 허용
    public String getName() {
        return this.name;
    }

    // setter에서 해당 데이터 수정
    public String setName(String name) {
        if (name != null &amp;&amp; name != &quot;&quot; &amp;&amp; name.length() &gt; 0) {
            this.name = name;
            return this.name;
        }
        return &quot;빈 문자열은 입력할 수 없습니다. 이름을 다시 입력해 주세요.&quot;;
    }
}
</code></pre>
<h3 id="주로-사용하는-경우">주로 사용하는 경우</h3>
<ul>
<li>클래스의 <strong>필드(멤버 변수)</strong> 거의 대부분에 사용</li>
<li>외부에 노출할 필요 없는 <strong>내부 로직 메서드</strong>에 사용</li>
<li>데이터 유효성 검사가 필요한 경우 setter와 함께 사용</li>
</ul>
<hr>
<h2 id="📦-default-package-private">📦 default (package-private)</h2>
<h3 id="특징-1">특징</h3>
<ul>
<li>접근 제어자를 <strong>아무것도 명시하지 않은</strong> 상태</li>
<li><strong>같은 패키지 내에서만</strong> 접근 가능</li>
<li>다른 패키지에서는 <code>import</code> 해도 접근 불가</li>
</ul>
<h3 id="예시-코드-1">예시 코드</h3>
<p>실제로는 아래처럼 패키지가 나뉘어 있을 때 동작한다.</p>
<pre><code>📁 com.example.service
    ├── UserService.java      ← 같은 패키지
    └── UserRepository.java  ← 같은 패키지

📁 com.example.controller
    └── UserController.java  ← 다른 패키지 → UserRepository 접근 불가</code></pre><p>같은 파일(같은 패키지)에서 default로 선언된 클래스와 메서드에 접근하는 예시다.</p>
<pre><code class="language-java">public class AccessModifier {
    public static void main(String[] args) {
        UserRepository repo = new UserRepository();  // ✅ 같은 패키지라 접근 가능
        System.out.println(repo.findById(1));        // ✅ &quot;User-1&quot; 출력

        // 다른 패키지에서 아래처럼 접근하면 컴파일 에러!
        // import com.example.service.UserRepository;
        // UserRepository repo = new UserRepository();  ❌
    }
}

class UserRepository {        // default 클래스 (접근 제어자 없음)
    String findById(int id) { // default 메서드 (접근 제어자 없음)
        return &quot;User-&quot; + id;
    }
}</code></pre>
<h3 id="주로-사용하는-경우-1">주로 사용하는 경우</h3>
<ul>
<li>패키지 내부에서만 사용할 <strong>유틸리티 클래스</strong></li>
<li>외부에 노출하고 싶지 않은 <strong>패키지 내부 구현체</strong></li>
<li>같은 패키지 내 클래스들이 협력할 때</li>
</ul>
<hr>
<h2 id="🛡️-protected">🛡️ protected</h2>
<h3 id="특징-2">특징</h3>
<ul>
<li><strong>같은 패키지</strong> + <strong>다른 패키지의 자식 클래스(상속)</strong>에서 접근 가능</li>
<li>상속 관계에서 부모의 기능을 자식이 사용/오버라이딩할 수 있게 열어줌</li>
<li><code>public</code>보다는 제한적, 상속을 의도한 설계에서 사용</li>
</ul>
<h3 id="예시-코드-2">예시 코드</h3>
<pre><code class="language-java">public class AccessModifier {
    public static void main(String[] args) {
        Dog dog = new Dog();
        dog.bark();
        // 출력:
        // 강아지이(가) 숨을 쉰다.
        // 강아지이(가) 짖는다.

        Animal animal = new Animal();
        // animal.name = &quot;고양이&quot;;  ❌ 상속받은 자식이 아니므로 외부에서 직접 접근 불가
        // animal.breathe();       ❌ 상속받은 자식이 아니므로 외부에서 직접 접근 불가
    }
}

class Animal {
    protected String name;  // 자식 클래스에서 접근 가능

    protected void breathe() {  // 자식 클래스에서 오버라이딩 가능
        System.out.println(name + &quot;이(가) 숨을 쉰다.&quot;);
    }
}

class Dog extends Animal {  // Animal을 상속받은 자식 클래스
    public void bark() {
        this.name = &quot;강아지&quot;;  // ✅ protected 필드 접근 가능 (상속)
        breathe();             // ✅ protected 메서드 호출 가능 (상속)
        System.out.println(name + &quot;이(가) 짖는다.&quot;);
    }
}</code></pre>
<h3 id="주로-사용하는-경우-2">주로 사용하는 경우</h3>
<ul>
<li><strong>상속을 설계</strong>할 때 자식 클래스에서만 접근 가능하도록 열어줄 때</li>
<li>템플릿 메서드 패턴에서 자식이 오버라이딩할 메서드</li>
<li>부모 클래스의 핵심 필드를 자식이 직접 사용해야 할 때</li>
</ul>
<hr>
<h2 id="🌍-public">🌍 public</h2>
<h3 id="특징-3">특징</h3>
<ul>
<li><strong>어디서든</strong> 접근 가능 (패키지, 상속 관계 무관)</li>
<li>API의 진입점, 외부에 공개할 기능에 사용</li>
<li>하나의 <code>.java</code> 파일에 <code>public</code> 클래스는 <strong>1개</strong>만 가능하며 파일명과 일치해야 함<h3 id="예시-코드-3">예시 코드</h3>
</li>
</ul>
<pre><code class="language-java">public class AccessModifier {
    public static void main(String[] args) {
        Person p = new Person();
        p.name = &quot;홍길동&quot;;  // 내부 변수 수정 가능
        System.out.println(p.name); // 내부 변수 바로 조회 가능

    }
}

class Person {
    public String name;  // 외부에서 직접 접근 불가

    // getter로 간접 접근 허용
    public String getName() {
        return this.name;
    }

    // setter에서 해당 데이터 수정
    public String setName(String name) {
        if (name != null &amp;&amp; name != &quot;&quot; &amp;&amp; name.length() &gt; 0) {
            this.name = name;
            return this.name;
        }
        return &quot;빈 문자열은 입력할 수 없습니다. 이름을 다시 입력해 주세요.&quot;;
    }
}</code></pre>
<h3 id="주로-사용하는-경우-3">주로 사용하는 경우</h3>
<ul>
<li>외부에 공개하는 <strong>API, 라이브러리의 진입점</strong></li>
<li>다른 패키지에서도 사용해야 하는 <strong>공통 유틸리티 메서드</strong></li>
<li><strong>클래스 자체</strong>를 외부에서 사용해야 할 때</li>
</ul>
<hr>
<h2 id="🔑-핵심-요약">🔑 핵심 요약</h2>
<blockquote>
<p><strong>&quot;최소한의 접근 권한을 부여하라&quot;</strong> (Principle of Least Privilege)</p>
</blockquote>
<ol>
<li><strong><code>private</code></strong>: 기본값으로 생각하자. 클래스 필드는 특별한 이유가 없으면 private</li>
<li><strong><code>default</code></strong>: 패키지 내부 구현 숨기기. 외부에 굳이 노출 안 해도 될 때</li>
<li><strong><code>protected</code></strong>: 상속 설계 시. 자식 클래스에만 열어줄 때</li>
<li><strong><code>public</code></strong>: 진짜 외부에 공개해야 할 API에만
접근 제어자를 제대로 활용하면 <strong>코드 변경의 파급 효과를 최소화</strong>하고, <strong>유지보수성</strong>이 크게 향상된다.</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Java] 객체지향 프로그래밍 — Car & 은행 계좌 클래스 설계]]></title>
            <link>https://velog.io/@jhwest-dev/Java-%EA%B0%9D%EC%B2%B4%EC%A7%80%ED%96%A5-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D-Car-%EC%9D%80%ED%96%89-%EA%B3%84%EC%A2%8C-%ED%81%B4%EB%9E%98%EC%8A%A4-%EC%84%A4%EA%B3%84</link>
            <guid>https://velog.io/@jhwest-dev/Java-%EA%B0%9D%EC%B2%B4%EC%A7%80%ED%96%A5-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D-Car-%EC%9D%80%ED%96%89-%EA%B3%84%EC%A2%8C-%ED%81%B4%EB%9E%98%EC%8A%A4-%EC%84%A4%EA%B3%84</guid>
            <pubDate>Mon, 20 Apr 2026 08:45:27 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>오늘은 클래스 설계 실습 문제 두 가지를 풀었다.<br>기초 문제에서는 Car 클래스를, 심화 문제에서는 은행 계좌 시스템을 구현했다.</p>
</blockquote>
<hr>
<h2 id="🚗-문제-1-car-클래스-만들기-기초">🚗 문제 1. Car 클래스 만들기 <code>기초</code></h2>
<h3 id="문제-요구사항">문제 요구사항</h3>
<ul>
<li><strong>속성</strong> : <code>brand</code>(제조사), <code>model</code>(모델명), <code>speed</code>(속도)</li>
<li><strong>메서드</strong><ul>
<li><code>accelerate(int amount)</code> — 속도 증가</li>
<li><code>breakSpeed(int amount)</code> — 속도 감소 (0 미만이면 0)</li>
<li><code>getInfo()</code> — 제조사, 모델명, 속도 출력</li>
</ul>
</li>
<li>Car 객체 2개를 만들어 accelerate, breakSpeed 후 getInfo 출력하기</li>
</ul>
<hr>
<h3 id="내-풀이">내 풀이</h3>
<pre><code class="language-java">class Car {
    private String brand;
    private String model;
    private int speed;

    public Car(String brand, String model, int speed) {
        this.brand = brand;
        this.model = model;
        this.speed = speed;
    }

    public void accelerate(int amount) {
        if (amount &lt; 0) {
            System.out.println(&quot;음수값은 입력이 불가합니다.&quot;);
        } else {
            this.speed = this.speed + amount;
            System.out.println(&quot;⬆️ 속도 &quot; + amount + &quot; 증가&quot;);
        }
    }

    public void breakSpeed(int amount) {
        if (amount &lt; 0) {
            System.out.println(&quot;음수는 입력이 불가합니다.&quot;);
        } else {
            this.speed = this.speed - amount;
            if (this.speed &lt; 0) { this.speed = 0; }
            System.out.println(&quot;⬇️ 속도 &quot; + amount + &quot; 감소&quot;);
        }
    }

    public void getInfo() {
        System.out.println(&quot;제조사 : &quot; + this.brand);
        System.out.println(&quot;모델명 : &quot; + this.model);
        System.out.println(&quot;속도 : &quot; + this.speed);
    }
}</code></pre>
<hr>
<h3 id="테스트-코드">테스트 코드</h3>
<pre><code class="language-java">Car car = new Car(&quot;테슬라&quot;, &quot;모델S&quot;, 1);
Car car2 = new Car(&quot;제네시스&quot;, &quot;GV70&quot;, 2);

System.out.println(&quot;-------- car 1 --------&quot;);
car.getInfo();
car.accelerate(9);
car.getInfo();
car.breakSpeed(4);
car.getInfo();

System.out.println(&quot;-------- car 2 --------&quot;);
car2.getInfo();
car2.accelerate(9);
car2.getInfo();
car2.breakSpeed(2);
car2.getInfo();</code></pre>
<h3 id="출력-결과">출력 결과</h3>
<pre><code>-------- car 1 --------
제조사 : 테슬라
모델명 : 모델S
속도 : 1
⬆️ 속도 9 증가
제조사 : 테슬라
모델명 : 모델S
속도 : 10
⬇️ 속도 4 감소
제조사 : 테슬라
모델명 : 모델S
속도 : 6
-------- car 2 --------
제조사 : 제네시스
모델명 : GV70
속도 : 2
⬆️ 속도 9 증가
제조사 : 제네시스
모델명 : GV70
속도 : 11
⬇️ 속도 2 감소
제조사 : 제네시스
모델명 : GV70
속도 : 9</code></pre><hr>
<h2 id="🏦-문제-2-은행-계좌-시스템-설계-심화">🏦 문제 2. 은행 계좌 시스템 설계 <code>심화</code></h2>
<h3 id="문제-요구사항-1">문제 요구사항</h3>
<ul>
<li><strong>속성</strong> : <code>balance</code>(잔액, private), <code>owner</code>(예금주)</li>
<li><strong>메서드</strong><ul>
<li><code>deposit(long amount)</code> — 입금 (음수면 무시)</li>
<li><code>withdraw(long amount)</code> — 출금 (잔액 부족 시 &quot;잔액 부족&quot; 출력)</li>
<li><code>transfer(BankAccount to, long amount)</code> — 이체 송금</li>
</ul>
</li>
<li>계좌 2개를 만들어 입금 → 이체 → 잔액 출력까지 구현하기</li>
</ul>
<hr>
<h3 id="내-풀이-1">내 풀이</h3>
<pre><code class="language-java">class BankAccount {
    private int balance;
    private String owner;

    public BankAccount(String owner, int balance) {
        this.balance = balance;
        this.owner = owner;
    }

    public void deposit(long amount) {
        if (amount &lt; 0) {
            System.out.println(&quot;값을 잘못 입력하였습니다.&quot;);
        } else {
            this.balance += amount;
            System.out.println(amount + &quot;원 입금이 완료되었습니다.&quot;);
            System.out.println(this.owner + &quot;님의 잔액은 총 &quot; + this.balance + &quot;원 입니다.&quot;);
        }
    }

    public void withdraw(long amount) {
        if (this.balance &lt; amount) {
            System.out.println(&quot;잔액 부족&quot;);
        } else {
            this.balance -= amount;
            System.out.println(amount + &quot;원 출금이 완료되었습니다.&quot;);
            System.out.println(this.owner + &quot;님의 잔액은 총 &quot; + this.balance + &quot;원 입니다.&quot;);
        }
    }

    public int transfer(BankAccount to, long amount) {
        String toOwner = to.owner;
        int toBalance = to.balance;

        if (this.balance &lt; amount) {
            System.out.println(&quot;잔액이 부족하여 이체가 불가 합니다. 현재 총 잔액 : &quot; + this.balance);
        } else if (amount == 0) {
            System.out.println(&quot;최소 1원이상 부터 이체가 가능합니다.&quot;);
        } else {
            this.balance -= amount;
            to.balance += amount;
            System.out.println(toOwner + &quot;님께 총&quot; + amount + &quot;원 이체가 완료 되었습니다.&quot;);
            return toBalance;
        }
        return 0;
    }

    public void getBalance() {
        System.out.println(&quot;이름 : &quot; + this.owner);
        System.out.println(&quot;총 남은 잔액 : &quot; + this.balance);
    }
}</code></pre>
<hr>
<h3 id="테스트-코드-1">테스트 코드</h3>
<pre><code class="language-java">BankAccount owner1 = new BankAccount(&quot;홍길동&quot;, 0);
owner1.deposit(100);
owner1.withdraw(50);

BankAccount owner2 = new BankAccount(&quot;김유신&quot;, 0);
owner2.deposit(300);
owner2.withdraw(100);
owner2.transfer(owner1, 200);

owner1.getBalance();
owner2.getBalance();</code></pre>
<h3 id="출력-결과-1">출력 결과</h3>
<pre><code>100원 입금이 완료되었습니다.
홍길동님의 잔액은 총 100원 입니다.
50원 출금이 완료되었습니다.
홍길동님의 잔액은 총 50원 입니다.

300원 입금이 완료되었습니다.
김유신님의 잔액은 총 300원 입니다.
100원 출금이 완료되었습니다.
김유신님의 잔액은 총 200원 입니다.

홍길동님께 총200원 이체가 완료 되었습니다.
이름 : 홍길동
총 남은 잔액 : 250
이름 : 김유신
총 남은 잔액 : 0</code></pre><hr>
<h2 id="💡-오늘-헷갈렸던-것--객체-참조-vs-값-복사">💡 오늘 헷갈렸던 것 — 객체 참조 vs 값 복사</h2>
<p><code>transfer()</code> 구현 중 상대 계좌의 잔액이 바뀌지 않는 문제가 발생했다.</p>
<p>원인은 객체의 필드값을 <strong>지역 변수에 복사</strong>하면 원본 객체와 연결이 끊긴다는 것.</p>
<pre><code class="language-java">// ❌ 지역 변수에 복사 — 원본 객체 잔액 그대로
int toBalance = to.balance;
toBalance += amount;

// ✅ 객체 직접 접근 — 실제 잔액 변경됨
to.balance += amount;</code></pre>
<blockquote>
<p>Java에서 <code>int</code> 같은 기본형은 값을 복사해서 전달한다.<br>객체의 필드를 지역 변수에 담으면 그 순간 원본과의 연결이 끊기기 때문에,<br>다른 객체의 상태를 변경하려면 객체 자체(<code>to.balance</code>)를 통해 직접 접근해야 한다.</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[[HTML/CSS/TypeScript] AI 채팅 화면 만들기(1) ]]></title>
            <link>https://velog.io/@jhwest-dev/HTMLCSSTypeScript%EB%A1%9C-AI-%EC%B1%84%ED%8C%85-%ED%99%94%EB%A9%B4-%EB%A7%8C%EB%93%A4%EA%B8%B01</link>
            <guid>https://velog.io/@jhwest-dev/HTMLCSSTypeScript%EB%A1%9C-AI-%EC%B1%84%ED%8C%85-%ED%99%94%EB%A9%B4-%EB%A7%8C%EB%93%A4%EA%B8%B01</guid>
            <pubDate>Thu, 16 Apr 2026 14:07:20 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>이번에는 부트캠프에서 배운 HTML/CSS/Typescript로 Gemini API를 이용한 AI 채팅 사이트를 만들어보았습니다. 
(디자인 구성은 Claude와 Gemini의 도움을 받았습니다.)</p>
</blockquote>
<hr>
<h2 id="1-전체-레이아웃-구조">1. 전체 레이아웃 구조</h2>
<p>AI 채팅 페이지의 레이아웃은 크게 <strong>사이드바(Sidebar)</strong> 와 <strong>메인 화면(Main)</strong> 두 영역으로 나뉩니다.</p>
<ul>
<li><strong>사이드바</strong> → 채팅 목록 관리</li>
<li><strong>메인 화면</strong> → 실제 채팅이 이루어지는 공간</li>
</ul>
<p><img src="https://velog.velcdn.com/images/jhwest-dev/post/661a47ce-b647-4cd5-96ec-04c7676d1c4c/image.png" alt=""></p>
<hr>
<h2 id="2-sidebar-구조">2. Sidebar 구조</h2>
<p>사이드바는 <strong>Header / Body / Footer</strong> 3단 구조입니다.</p>
<table>
<thead>
<tr>
<th>영역</th>
<th>내용</th>
</tr>
</thead>
<tbody><tr>
<td>Header</td>
<td>새 채팅(New Chat) 버튼 + 검색창</td>
</tr>
<tr>
<td>Body</td>
<td>이전 채팅 목록</td>
</tr>
<tr>
<td>Footer</td>
<td>내 프로필 + 설정</td>
</tr>
</tbody></table>
<hr>
<h2 id="3-main-구조">3. Main 구조</h2>
<p>메인화면도 동일하게 <strong>Header / Body / Footer</strong> 3단 구조입니다.</p>
<table>
<thead>
<tr>
<th>영역</th>
<th>내용</th>
</tr>
</thead>
<tbody><tr>
<td>Header</td>
<td>로고 + 필터</td>
</tr>
<tr>
<td>Body</td>
<td>채팅 메시지 영역</td>
</tr>
<tr>
<td>Footer</td>
<td>질문 입력창</td>
</tr>
</tbody></table>
<hr>
<h2 id="4-html-레이아웃-구성">4. HTML 레이아웃 구성</h2>
<p>CSS를 입히기 전, HTML만으로 먼저 골격을 잡았습니다.</p>
<pre><code>&lt;div&gt;
  &lt;header class=&quot;main-header&quot;&gt;...&lt;/header&gt;

  &lt;main&gt;
    &lt;section class=&quot;prompt-section&quot;&gt;...&lt;/section&gt;   &lt;!-- 프롬프트 필터 + 카드 --&gt;
    &lt;section class=&quot;chat-messages&quot;&gt;...&lt;/section&gt;     &lt;!-- 대화 메시지 영역 --&gt;
    &lt;section class=&quot;question-section&quot;&gt;...&lt;/section&gt;  &lt;!-- 질문 입력창 --&gt;
  &lt;/main&gt;

  &lt;aside class=&quot;chat-sessions-sidebar&quot;&gt;...&lt;/aside&gt;   &lt;!-- 채팅 목록 사이드바 --&gt;
  &lt;div class=&quot;sidebar-overlay&quot;&gt;&lt;/div&gt;                &lt;!-- 사이드바 오버레이 --&gt;
&lt;/div&gt;</code></pre><p><img src="https://velog.velcdn.com/images/jhwest-dev/post/b176187e-c6f5-4dfa-bdee-4cd7ecf77099/image.png" alt=""></p>
<hr>
<h2 id="5-css-변수-custom-properties">5. CSS 변수 (Custom Properties)</h2>
<p>CSS 변수는 <code>:root</code>에 정의하면 파일 전체에서 <code>var(--이름)</code> 형태로 사용할 수 있습니다.<br>색상과 크기를 한 곳에서 관리하기 때문에 <strong>유지보수가 훨씬 쉬워집니다.</strong></p>
<pre><code class="language-css">:root {
    --color-bg: #fff5f7; /* 페이지 배경 색상 */
    --color-sidebar-bg: #fce4ec; /* 사이드바 배경 색상 */
    --color-header-bg: #ffffff; /* 메인 헤더 배경 색상 */
    --color-header-logo: #ff97af; /* 메인 헤더 로고 색상 */
    --color-accent: #ffb3c6; /* 강조 */
    --color-text: #7b3a52; /* 기본 텍스트 색상 */
    --color-border: #f4c0d1; /* 테두리 색상 */

    --font-main: &quot;Segoe UI&quot;, &quot;Apple SD Gothic Neo&quot;, &quot;snas-serif&quot;;
    --radius: 12px;
    --shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
    --transition: 0.2s ease;
}</code></pre>
<pre><code>// 사용법
.body {
    color: var(--color-text);
}</code></pre><p><img src="https://velog.velcdn.com/images/jhwest-dev/post/4354520d-a3d7-4105-85ad-8b79e8a2449c/image.png" alt="">
<em>CSS로 배경과 폰트 색상을 수정해 보았습니다.</em></p>
<hr>
<h2 id="6-다음-단계">6. 다음 단계</h2>
<p>다음 글에서는 아래 내용을 순서대로 다룰 예정입니다.</p>
<ol>
<li>CSS로 레이아웃 잡기 (Flexbox / Grid)</li>
<li>TypeScript로 채팅, 대화 목록, 필터링 구현</li>
<li>Gemini API 연결</li>
</ol>
<hr>
<blockquote>
<p>이 포스팅은 시리즈로 이어집니다. 다음 글에서는 스타일 코드 작성과, TypeScript로 간단한 채팅 기능을 구현해 보겠습니다.</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[[TypeScript] 기초 개념 정리]]></title>
            <link>https://velog.io/@jhwest-dev/Day-6-TypeScript-%EA%B8%B0%EC%B4%88-%EA%B0%9C%EB%85%90-%EC%A0%95%EB%A6%AC</link>
            <guid>https://velog.io/@jhwest-dev/Day-6-TypeScript-%EA%B8%B0%EC%B4%88-%EA%B0%9C%EB%85%90-%EC%A0%95%EB%A6%AC</guid>
            <pubDate>Tue, 14 Apr 2026 12:20:17 GMT</pubDate>
            <description><![CDATA[<h2 id="1-typescript란">1. TypeScript란?</h2>
<p><strong>실행 전에</strong> 코드 작성 단계에서 타입 에러를 미리 잡아주는 언어.
브라우저는 TS를 직접 실행할 수 없으므로, <code>tsc</code> 명령으로 컴파일해 <code>.js 파일</code>로 변환 후 사용한다.</p>
<pre><code class="language-bash">// 예시
tsc app.ts</code></pre>
<hr>
<h2 id="2-타입-선언-기본-문법">2. 타입 선언 기본 문법</h2>
<pre><code class="language-ts">// 기본 타입 → 변수명 : 타입 = 값;
const name: string = &quot;홍길동&quot;;
const age: number = 20;
const subjects: string[] = [&quot;국어&quot;, &quot;영어&quot;, &quot;수학&quot;];

// Union → | 로 두 가지 이상 허용
// 변수에 string 타입의 값을 할당하고, 추후 number 값을 할당할 수 있음
let userId: string | number = &#39;id-123&#39;;
userId = 123;

// Optional → ? 로 값이 없어도 가능
let nickname?: string;

// Literal 타입 → 정해진 값만 허용
let color: &#39;red&#39; | &#39;blue&#39; | &#39;black&#39; = &#39;blue&#39;;
color = &#39;pink&#39;; // Error: &#39;pink&#39;는 허용되지 않음</code></pre>
<hr>
<h2 id="3-type-alias">3. type alias</h2>
<p>자주 쓰는 타입 조합에 이름을 붙여 재사용한다.</p>
<pre><code class="language-ts">// 1. type 키워드로 이름 정의
type Color = &#39;red&#39; | &#39;blue&#39; | &#39;black&#39;;

// 2. 정의한 이름으로 해당 타입 사용 가능
let color: Color = &#39;red&#39;;</code></pre>
<hr>
<h2 id="4-interface">4. Interface</h2>
<p>객체가 가져야 할 프로퍼티를 정의한다. (객체의 설계도)</p>
<pre><code class="language-ts">interface Employee {
  id: number;
  name: string;
  department: string;
}</code></pre>
<h3 id="컴파일-후-사라지는-이유">컴파일 후 사라지는 이유</h3>
<p>Interface는 <strong>타입 검사 전용</strong> 도구다. JavaScript에는 &quot;interface&quot;라는 개념이 없기 때문에, TypeScript 컴파일러가 타입 검사를 마치면 JS로 변환될 때 완전히 제거된다. 런타임에 어떤 흔적도 남기지 않아 번들 크기에도 영향을 주지 않는다.</p>
<pre><code class="language-ts">// TypeScript (.ts)
interface Employee {
  id: number;
  name: string;
}
const emp: Employee = { id: 1, name: &quot;김철수&quot; };

// 컴파일 결과 (.js) — interface 흔적 없음
const emp = { id: 1, name: &quot;김철수&quot; };</code></pre>
<blockquote>
<p><strong>Interface vs Class</strong>
Interface는 <em>설계도</em>만 정의하고, Class는 <em>실제 동작하는 코드</em>를 만든다.
<code>implements</code> 키워드로 클래스가 인터페이스를 따르도록 강제할 수 있다.</p>
</blockquote>
<hr>
<h2 id="5-class">5. Class</h2>
<p><code>public</code> / <code>private</code> / <code>readonly</code> 로 각 프로퍼티의 접근 범위를 제한한다.</p>
<pre><code class="language-ts">class Employee {
  public   name: string;      // 어디서든 접근 가능 (기본값)
  private  salary: number;    // 클래스 내부에서만 접근 가능
  readonly id: number;        // 읽기 전용 — 초기화 후 변경 불가

  // 생성자 매개변수에 접근 제어자를 쓰면 선언 + 할당 동시에
  constructor(
    public department: string,
    name: string,
    salary: number,
    id: number
  ) {
    this.name   = name;
    this.salary = salary;
    this.id     = id;
  }

  // private 값은 getter 메서드로만 노출
  getSalary(): number {
    return this.salary;
  }
}

const emp = new Employee(&quot;개발팀&quot;, &quot;이영희&quot;, 5000, 1);

emp.name;           // OK — public
emp.department;     // OK — public (생성자 단축 선언)
emp.getSalary();    // OK — 5000
emp.salary;         // Error: private 프로퍼티
emp.id = 99;        // Error: readonly 프로퍼티</code></pre>
<hr>
<h2 id="6-제네릭">6. 제네릭</h2>
<p>T는 어떤 타입이든 넣을 수 있는 &#39;빈 박스&#39;와 같다.
함수나 인터페이스를 정의할 때 타입을 미리 지정하지 않고, 실제로 사용할 때 타입을 결정하여 코드의 재사용성을 높인다.</p>
<pre><code class="language-ts">function identity&lt;T&gt;(value: T): T {
  return value;
}

identity&lt;string&gt;(&quot;hello&quot;);  // T = string
identity&lt;number&gt;(42);       // T = number</code></pre>
<h3 id="실용-예시-api-응답-래퍼">실용 예시: API 응답 래퍼</h3>
<pre><code class="language-ts">interface ApiResponse&lt;T&gt; {
  data:    T;
  status:  number;
  message: string;
}

interface User { id: number; name: string; }
interface Post { title: string; body: string; }

// 같은 구조, 다른 data 타입
const userRes: ApiResponse&lt;User&gt; = {
  data: { id: 1, name: &quot;박민준&quot; },
  status: 200,
  message: &quot;ok&quot;,
};

const postRes: ApiResponse&lt;Post&gt; = {
  data: { title: &quot;TypeScript 입문&quot;, body: &quot;...&quot; },
  status: 200,
  message: &quot;ok&quot;,
};</code></pre>
<hr>
<h2 id="7-네이밍-규칙">7. 네이밍 규칙</h2>
<table>
<thead>
<tr>
<th>종류</th>
<th>표기법</th>
<th>예시</th>
</tr>
</thead>
<tbody><tr>
<td>기본 내장 타입</td>
<td>소문자</td>
<td><code>string</code>, <code>number</code>, <code>boolean</code></td>
</tr>
<tr>
<td>type alias</td>
<td>PascalCase</td>
<td><code>Status</code>, <code>Subject</code>, <code>UserId</code></td>
</tr>
<tr>
<td>interface</td>
<td>PascalCase</td>
<td><code>UserInfo</code>, <code>ApiResponse</code></td>
</tr>
</tbody></table>
<hr>
<h2 id="8-정리">8. 정리</h2>
<p><strong>01. TypeScript는 JavaScript에 안전망을 씌운 것</strong>
런타임 에러를 작성 단계에서 발견하면 디버깅 시간이 크게 줄어든다.</p>
<p><strong>02. 팀 프로젝트에서 빛을 발한다</strong>
타입 선언은 처음엔 번거롭게 느껴지지만, 팀 프로젝트에서 코드를 처음 보는 사람도 타입만 보면 의도를 파악할 수 있다. 결국 타입이 곧 문서다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[집에 있는 계산기를 웹으로 옮겨보았다 +계산기 로직 분석]]></title>
            <link>https://velog.io/@jhwest-dev/%EC%A7%91%EC%97%90-%EC%9E%88%EB%8A%94-%EA%B3%84%EC%82%B0%EA%B8%B0%EB%A5%BC-%EC%9B%B9%EC%9C%BC%EB%A1%9C-%EC%98%AE%EA%B2%A8%EB%B3%B4%EC%95%98%EB%8B%A4-JS-%EB%A1%9C%EC%A7%81-%EB%B6%84%EC%84%9D</link>
            <guid>https://velog.io/@jhwest-dev/%EC%A7%91%EC%97%90-%EC%9E%88%EB%8A%94-%EA%B3%84%EC%82%B0%EA%B8%B0%EB%A5%BC-%EC%9B%B9%EC%9C%BC%EB%A1%9C-%EC%98%AE%EA%B2%A8%EB%B3%B4%EC%95%98%EB%8B%A4-JS-%EB%A1%9C%EC%A7%81-%EB%B6%84%EC%84%9D</guid>
            <pubDate>Mon, 13 Apr 2026 16:05:09 GMT</pubDate>
            <description><![CDATA[<p>이번 수업에서는 자바스크립트로 계산기와 그림판을 만들어보았다.</p>
<p>계산기를 직접 다시 구현해 보면서, 집에 있는 실물 계산기를 모델 삼아 CSS로 최대한 비슷하게 재현해 보았다.</p>
<p>CSS를 거의 처음 다뤄보다 보니 레이아웃을 어떻게 잡아야 할지 감이 잘 오지 않아 선생님 코드를 참고하면서 속성을 하나씩 추가하고, 브라우저에서 바로 확인하는 방식으로 작업했다.</p>
<p>그러다 보니 자연스럽게 이 속성이 뭘 하는 건지 찾아보게 됐고, 특히 + 버튼을 두 칸 차지하게 만드는 부분에서 grid-row: span 2를 처음 알게 됐다.</p>
<hr>


<h2 id="계산기-핵심-로직-분석">계산기 핵심 로직 분석</h2>
<h3 id="1-상태-관리">1. 상태 관리</h3>
<pre><code class="language-javascript">let currentInput = &quot;0&quot;;
let previousInput = &quot;&quot;;
let operator = null;
let shouldReset = false;</code></pre>
<p>계산기의 상태는 네 가지 변수로 관리된다. <code>currentInput</code>은 현재 화면에 보이는 숫자, <code>previousInput</code>은 연산자 앞에 입력된 숫자, <code>operator</code>는 선택된 연산자를 저장한다.</p>
<p><code>shouldReset</code>은 연산자를 누른 직후 다음 숫자 입력 시 화면을 초기화할지 여부를 나타내는 플래그다.</p>
<p>예를 들어 <code>5 + 3 =</code> 을 입력하면, <code>5</code>를 누르면 <code>currentInput = &quot;5&quot;</code>, <code>+</code>를 누르면 <code>previousInput = &quot;5&quot;</code>, <code>operator = &quot;+&quot;</code>, <code>shouldReset = true</code>로 바뀐다. 이후 <code>3</code>을 누르면 <code>shouldReset</code>이 <code>true</code>이므로 화면을 초기화하고 <code>currentInput = &quot;3&quot;</code>으로 새로 시작한다. 마지막으로 <code>=</code>를 누르면 계산이 실행된다.</p>
<h3 id="2-숫자-입력">2. 숫자 입력</h3>
<pre><code class="language-javascript">function inputNum(num) {
    if (shouldReset) {
        currentInput = num;
        shouldReset = false;
    } else {
        if (currentInput.length &gt;= 10) return;
        currentInput = currentInput === &quot;0&quot; ? num : currentInput + num;
    }
    updateDisplay();
}</code></pre>
<p>숫자 버튼을 눌렀을 때 실행되는 함수다. 두 가지 경우를 처리한다.</p>
<p>첫 번째로 <code>shouldReset</code>이 <code>true</code>이면, 연산자를 누른 직후이므로 화면을 초기화하고 새 숫자로 시작한다.</p>
<p>두 번째 삼항 연산자로 현재값이 <code>&quot;0&quot;</code>인지 확인한다. <code>&quot;0&quot;</code>이면 새 숫자로 교체하고, <code>&quot;0&quot;</code>이 아니면 뒤에 이어 붙인다. <code>&quot;0&quot;</code> 상태에서 그냥 뒤에 붙이면 <code>&quot;09&quot;</code> 같은 숫자가 되기 때문이다.</p>
<h3 id="3-연산자-입력">3. 연산자 입력</h3>
<pre><code class="language-javascript">function setOperator(op) {
    if (operator &amp;&amp; !shouldReset) {
        calculate();
    }
    previousInput = currentInput;
    operator = op;
    shouldReset = true;

    const symbols = { &quot;+&quot;: &quot;+&quot;, &quot;-&quot;: &quot;-&quot;, &quot;*&quot;: &quot;X&quot;, &quot;/&quot;: &quot;÷&quot; };
    expressionEl.textContent = `${previousInput} ${symbols[op]}`
}</code></pre>
<p>연산자 버튼을 눌렀을 때 실행되는 함수다. <code>3 + 5 *</code> 처럼 숫자 입력 후 연산자를 또 누르면 <code>3 + 5</code> 를 먼저 계산하고 <code>*</code> 로 이어간다.</p>
<p><code>symbols</code> 객체는 <code>*</code> 를 <code>X</code>, <code>/</code> 를 <code>÷</code> 로 변환해 사용자에게 계산식을 보기 좋게 화면에 표시하기 위해 사용한다.</p>
<h3 id="4-계산">4. 계산</h3>
<pre><code class="language-javascript">function calculate() {
    if (!operator || !previousInput) return;

    const prev = parseFloat(previousInput);
    const curr = parseFloat(currentInput);
    let result;   

    switch (operator) {
        case &quot;+&quot;: result = prev + curr; break;
        case &quot;-&quot;: result = prev - curr; break;
        case &quot;*&quot;: result = prev * curr; break;
        case &quot;/&quot;:
            if (curr === 0) {
                currentInput = &quot;오류&quot;;
                expressionEl.textContent = &quot;&quot;;
                operator = null;
                updateDisplay();
                return;
            }
            result = prev / curr;
            break;
    }
}</code></pre>
<p><code>=</code> 버튼을 눌렀을 때 실행되는 함수다. <code>currentInput</code>을 문자열로 관리하는 이유는 사용자가 <code>893</code>을 입력할 때 숫자로 받으면 덧셈이 되어버리기 때문이다. 그래서 문자열로 받고 <code>parseFloat</code>으로 변환해서 계산한다.</p>
<p><code>/</code> 연산에서 <code>curr</code>이 <code>0</code>이면 수학적으로 정의할 수 없으므로 &quot;오류&quot;를 표시하고 함수를 종료한다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[JavaScript] 시작하기]]></title>
            <link>https://velog.io/@jhwest-dev/Day-4-Javascript</link>
            <guid>https://velog.io/@jhwest-dev/Day-4-Javascript</guid>
            <pubDate>Thu, 09 Apr 2026 14:33:49 GMT</pubDate>
            <description><![CDATA[<h3 id="자바스크립트란">자바스크립트란?</h3>
<p>사용자의 행동에 반응하고, 데이터를 처리하며, 웹페이지를 살아있게 만드는 프로그래밍 언어</p>
<p>HTML이 뼈대를 만들고, CSS가 꾸며주면, JavaScript는 거기에 생명을 불어넣는 역할</p>
<p>버튼을 누르면 반응하고, 데이터를 계산하고, 서버와 소통하고, 화면을 실시간으로 바꾸는 등 웹에서 일어나는 모든 동적인 것들을 말한다.</p>
<blockquote>
<p><strong>HTML</strong> = 사람의 몸 (뼈, 장기)
<strong>CSS</strong>  = 사람의 외모 (옷, 헤어스타일)
<strong>JS</strong>   = 사람의 뇌 + 근육 (생각하고, 판단하고, 움직임)</p>
</blockquote>
<hr>



<h3 id="script-태그는-어디에-위치해야-할까">script 태그는 어디에 위치해야 할까?</h3>
<p>HTML 파일에서 JavaScript를 불러올 때 script 태그의 위치를 어디에 놓아야 좋을지 궁금했다.</p>
<p><strong>1. head 안에 넣는 경우</strong></p>
<pre><code>&lt;head&gt;
  &lt;script src=&quot;app.js&quot;&gt;&lt;/script&gt;
&lt;/head&gt;</code></pre><pre><code>HTML이 다 그려지기 전에 JS가 실행되어 오류가 날 수 있다.</code></pre><p><strong>2. body 맨 아래에 넣는 경우</strong></p>
<pre><code>&lt;body&gt;
  &lt;h1&gt;Hello&lt;/h1&gt;
  &lt;script src=&quot;app.js&quot;&gt;&lt;/script&gt;
&lt;/body&gt;</code></pre><pre><code>- HTML을 다 그린 후 JS가 실행되어 안전하다.
- JS 파일이 크면 페이지 로딩이 느려질 수 있다.</code></pre><p><strong>3. head 안에 async 사용</strong></p>
<pre><code>&lt;head&gt;
  &lt;script async src=&quot;app.js&quot;&gt;&lt;/script&gt;
  &lt;script async src=&quot;util.js&quot;&gt;&lt;/script&gt;
  &lt;script async src=&quot;main.js&quot;&gt;&lt;/script&gt;
&lt;/head&gt;</code></pre><pre><code>- HTML 로딩과 JS 로딩을 동시에 진행한다.
- JS 로딩이 끝나면 즉시 실행한다.
- 순서가 보장되지 않아 JS끼리 의존성이 있으면 위험하다.</code></pre><p><strong>4. head 안에 defer 사용</strong></p>
<pre><code>&lt;head&gt;
  &lt;script defer src=&quot;app.js&quot;&gt;&lt;/script&gt;
  &lt;script defer src=&quot;util.js&quot;&gt;&lt;/script&gt;
  &lt;script defer src=&quot;main.js&quot;&gt;&lt;/script&gt;
&lt;/head&gt;</code></pre><pre><code>- HTML 로딩과 JS 로딩을 동시에 진행한다.
- HTML이 다 그려진 후에 JS가 실행되므로 오류가 날 위험이 없다.
- script 태그가 여러 개일 때 순서가 보장된다.</code></pre><hr>

<h3 id="변수-선언">변수 선언</h3>
<p>변수란? 데이터를 담아두는 이름표가 붙은 상자</p>
<table>
<thead>
<tr>
<th></th>
<th><code>var</code></th>
<th><code>let</code></th>
<th><code>const</code></th>
</tr>
</thead>
<tbody><tr>
<td>재선언</td>
<td>✅ 가능</td>
<td>❌ 불가</td>
<td>❌ 불가</td>
</tr>
<tr>
<td>재할당</td>
<td>✅ 가능</td>
<td>✅ 가능</td>
<td>❌ 불가</td>
</tr>
<tr>
<td>사용 권장</td>
<td>❌ 지양 (버그 위험)</td>
<td>✅ 권장</td>
<td>✅ 권장</td>
</tr>
<tr>
<td><br></td>
<td></td>
<td></td>
<td></td>
</tr>
</tbody></table>
<h4 id="각각-언제-사용할까">각각 언제 사용할까?</h4>
<ol>
<li><p><strong>var</strong> - 구버전으로 사용 지양
var x = 1;
var x = 2; // 재선언 가능</p>
</li>
<li><p><strong>let</strong> - 나중에 값이 바뀔 때 사용
let count = 1;
count = 3; // 재할당 가능</p>
</li>
<li><p><strong>const</strong> - 값이 고정일 때 (기본적으로 이걸 먼저 사용하기)
const name = &quot;철수&quot;;
name = &quot;영희&quot;; // ❌ 오류 발생!</p>
</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[[CSS & Bootstrap] 옷 직접 만들기 vs 사 입기]]></title>
            <link>https://velog.io/@jhwest-dev/Day-2-3-CSS-Bootstrap-%EC%82%AC%EC%9A%A9%ED%95%B4%EB%B3%B4%EA%B8%B0</link>
            <guid>https://velog.io/@jhwest-dev/Day-2-3-CSS-Bootstrap-%EC%82%AC%EC%9A%A9%ED%95%B4%EB%B3%B4%EA%B8%B0</guid>
            <pubDate>Wed, 08 Apr 2026 15:26:14 GMT</pubDate>
            <description><![CDATA[<h3 id="css란">CSS란?</h3>
<p>구조만 있는 HTML 텍스트에 스타일을 입혀주는 언어
ex) html - 사람, css - 옷</p>
<ul>
<li>HTML 문서에 &#39;style&#39; 태그 안에 삽입할 수도 있고</li>
<li>별도의 파일로 분리하는 것도 가능</li>
</ul>
<hr>


<h3 id="css-선택자-우선순위">css 선택자 우선순위</h3>
<p>id &gt; class &gt; tag</p>
<hr>

<h3 id="박스-모델box-model">박스 모델(Box Model)</h3>
<p><img src="https://velog.velcdn.com/images/jhwest-dev/post/73f94cde-e7fd-40cd-aeb5-60517962cb82/image.png" alt=""></p>
<ol>
<li>content : 텍스트, 내용</li>
<li>padding : 텍스트와 테두리의 공간</li>
<li>border : 테두리</li>
<li>margin : 요소 바깥의 외부 공간</li>
</ol>
<hr>


<h3 id="🔎-css-사용해보기">🔎 CSS 사용해보기</h3>
<h4 id="1-외부-css-파일-연결해서-사용해보기">1. 외부 CSS 파일 연결해서 사용해보기</h4>
<pre><code>1) style.css 파일 생성 
2) head 태그 안에 아래와 같은 코드를 추가하여 사용</code></pre><pre><code>&lt;link rel=&quot;stylesheet&quot; href=&quot;style.css&quot;&gt;</code></pre><h4 id="2-내부-head--style-태그-안에서-사용해보기">2. 내부 head &gt; style 태그 안에서 사용해보기</h4>
<pre><code>&lt;head&gt;
    &lt;meta charset=&quot;UTF-8&quot;&gt;
    &lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1.0&quot;&gt;
    &lt;title&gt;CSS 사용해보기&lt;/title&gt;
    &lt;style&gt;
        /* 스타일 추가 */
        body {
            font-family: &#39;Chiron GoRound TC&#39;;
            background: rgb(255, 254, 246);
            color: rgb(21, 20, 20);
        }
    &lt;/style&gt;

&lt;/head&gt;</code></pre><hr>

<h3 id="bootstrap이란">Bootstrap이란?</h3>
<p><strong>CSS가</strong> 옷을 직접 만들고 코디해서 입는 거라면
<strong>Bootstrap은</strong> 이미 만들어져 있는 옷을 코디해서 입는 거라고 이해했다.</p>
<hr>

<h3 id="🔎-부트스트랩-사용해보기">🔎 부트스트랩 사용해보기</h3>
<h4 id="별도의-설치-없이-cdn으로-바로-사용-가능">별도의 설치 없이 CDN으로 바로 사용 가능</h4>
<ol>
<li><p>부트스트랩 홈페이지 접속 &gt; Docs 페이지에서 CDN 링크 복사
<a href="https://getbootstrap.com/docs/5.3/getting-started/introduction/">https://getbootstrap.com/docs/5.3/getting-started/introduction/</a>
<img src="https://velog.velcdn.com/images/jhwest-dev/post/fda6e5a6-8382-41c7-a3e1-27d0e69db7af/image.png" alt=""></p>
<br>
</li>
<li><p>아래 코드와 같이 head 태그 안에 링크 붙여넣기
 *참고 : rel=&quot;stylesheet&quot;가 없으면 브라우저가 이게 CSS 파일인지 몰라서 적용을 안 해서 추가해 줘야 함</p>
<pre><code>&lt;head&gt;
 &lt;meta charset=&quot;UTF-8&quot;&gt;
 &lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1.0&quot;&gt;
 &lt;title&gt;Bootstrap&lt;/title&gt;
 &lt;link rel=&quot;stylesheet&quot; href=&quot;https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css&quot;&gt;
&lt;/head&gt;</code></pre></li>
</ol>
<br>

<ol start="3">
<li><p>부트스트랩에서 사용하고 싶은 컴포넌트 복사 후 body 태그 안에 붙여 넣으면 사용 가능
<img src="https://velog.velcdn.com/images/jhwest-dev/post/4fa89df3-4483-4a9e-8301-3e3070fd5ac8/image.png" alt=""></p>
<pre><code>&lt;body&gt;
   &lt;button type=&quot;button&quot; class=&quot;btn btn-primary&quot;&gt;Primary&lt;/button&gt;
   &lt;button type=&quot;button&quot; class=&quot;btn btn-secondary&quot;&gt;Secondary&lt;/button&gt;
   &lt;button type=&quot;button&quot; class=&quot;btn btn-success&quot;&gt;Success&lt;/button&gt;
   &lt;button type=&quot;button&quot; class=&quot;btn btn-danger&quot;&gt;Danger&lt;/button&gt;
   &lt;button type=&quot;button&quot; class=&quot;btn btn-warning&quot;&gt;Warning&lt;/button&gt;
   &lt;button type=&quot;button&quot; class=&quot;btn btn-info&quot;&gt;Info&lt;/button&gt;
   &lt;button type=&quot;button&quot; class=&quot;btn btn-light&quot;&gt;Light&lt;/button&gt;
   &lt;button type=&quot;button&quot; class=&quot;btn btn-dark&quot;&gt;Dark&lt;/button&gt;

   &lt;button type=&quot;button&quot; class=&quot;btn btn-link&quot;&gt;Link&lt;/button&gt;
&lt;/body&gt;</code></pre><br>
4. 결과
![](https://velog.velcdn.com/images/jhwest-dev/post/8c965fbe-fc32-4f83-be08-9ec8e8ef422a/image.png)


</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[[HTML] 기본 구조와 태그]]></title>
            <link>https://velog.io/@jhwest-dev/Day-1-HTML-%EC%8B%9C%EC%9E%91%ED%95%98%EA%B8%B0-%EA%B8%B0%EB%B3%B8-%EA%B5%AC%EC%A1%B0%EC%99%80-%ED%83%9C%EA%B7%B8</link>
            <guid>https://velog.io/@jhwest-dev/Day-1-HTML-%EC%8B%9C%EC%9E%91%ED%95%98%EA%B8%B0-%EA%B8%B0%EB%B3%B8-%EA%B5%AC%EC%A1%B0%EC%99%80-%ED%83%9C%EA%B7%B8</guid>
            <pubDate>Mon, 06 Apr 2026 14:36:55 GMT</pubDate>
            <description><![CDATA[<h3 id="html이란-hypertext-markup-language"><strong>HTML이란? (HyperText Markup Language)</strong></h3>
<p>간단히 말하면 웹페이지의 구조를 태그를 이용하여 정의하는 언어</p>
<blockquote>
</blockquote>
<ul>
<li>HyperText : 하이퍼링크를 통해 다른 페이지로 이동할 수 있는 텍스트</li>
<li>Markup: 태그를 이용하여 텍스트에게 역할을 주는 것 Ex) 제목, 줄바꿈, 비디오 등</li>
<li>Language : 브라우저가 이해할 수 있는 언어</li>
</ul>
<br>

<h3 id="html-문서의-기본-구조">html 문서의 기본 구조</h3>
<pre><code>&lt;!DOCTYPE html&gt; 
&lt;html lang=&quot;en&quot;&gt;
&lt;head&gt;
    &lt;meta charset=&quot;UTF-8&quot;&gt;
    &lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1.0&quot;&gt;
    &lt;title&gt;Document&lt;/title&gt;
&lt;/head&gt;
&lt;body&gt; 
&lt;/body&gt;
&lt;/html&gt;</code></pre><ul>
<li><strong>!DOCTYPE html :</strong> 현재 문서가 HTML 언어로 작성한 웹 문서</li>
<li><strong>html :</strong> 웹 문서의 시작과 끝</li>
<li>** lang=&quot;en&quot; :** 페이지가 어떤 언어로 작성됐는지 명시. </li>
<li><strong>head :</strong> 메타데이터와 웹페이지의 타이틀</li>
<li><strong>meta charset=&quot;UTF-8&quot; :</strong> 문자 인코딩 방식을 UTF-8로 설정하는 것. UTF-8은 대부분의 언어를 지원하기에 한글이 깨지지않음.</li>
<li><strong>width=device-width :</strong> 페이지의 너비를 기기 너비에 맞춤</li>
<li>*<em>initial-scale=1.0 : *</em> 처음 화면을 보여줄 때 1배율로 보여줌</li>
<li><strong>title :</strong> 웹 브라우저 탭에 뜨는 제목</li>
<li><strong>body :</strong> 브라우저 화면에 보이는 부분</li>
</ul>
<br>

<h3 id="오늘-배운-태그"><strong>오늘 배운 태그</strong></h3>
<table>
<thead>
<tr>
<th>종류</th>
<th>태그</th>
</tr>
</thead>
<tbody><tr>
<td>시맨틱 태그</td>
<td>header, main, footer, section, article</td>
</tr>
<tr>
<td>텍스트/목록</td>
<td>h1~h5, ul, br</td>
</tr>
<tr>
<td>미디어</td>
<td>video controls, audio controls</td>
</tr>
<tr>
<td>폼</td>
<td>form, fieldset, legend, label, input, textarea, button</td>
</tr>
</tbody></table>
<p>시맨틱 태그(Semantic Tag)
:태그 이름 자체에 의미가 담겨 있음
ex) header
시맨틱 태그를 쓰는 이유는?
다른 개발자들과 협업을 할 때 코드의 가독성을 높인다.</p>
<p>배운 내용을 바탕으로 아래와 같이 나의 자기소개서 구조를 만들어봤다.
<img src="https://velog.velcdn.com/images/jhwest-dev/post/c90070ea-474c-4ca2-8926-e40cd15d7cc7/image.png" alt=""></p>
<pre><code>&lt;!DOCTYPE html&gt;
&lt;html lang=&quot;ko&quot;&gt;
&lt;head&gt;
    &lt;meta charset=&quot;UTF-8&quot;&gt;
    &lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1.0&quot;&gt;
    &lt;title&gt;About Me - 서지현&lt;/title&gt;
&lt;/head&gt;

&lt;body&gt;
    &lt;!-- 헤더 : 페이지 상단 영역 --&gt;
    &lt;header&gt;
        &lt;h1&gt;jhwest.dev&lt;/h1&gt;
        &lt;nav&gt;
            &lt;ul&gt;
                &lt;li&gt;&lt;a href=&quot;#about&quot;&gt;About Me&lt;/a&gt;
                &lt;li&gt;&lt;a href=&quot;#favorites&quot;&gt;Favorites&lt;/a&gt;
                &lt;li&gt;&lt;a href=&quot;#experience&quot;&gt;Experience&lt;/a&gt;
                &lt;li&gt;&lt;a href=&quot;#guestbook&quot;&gt;Guestbook&lt;/a&gt;
            &lt;/ul&gt;
        &lt;/nav&gt;
    &lt;/header&gt;

    &lt;!-- main: 페이지의 내용 부분 --&gt;
    &lt;main&gt;
        &lt;!-- section: 주제별 구역 --&gt;
        &lt;section id=&quot;about&quot;&gt;
            &lt;h2&gt;ABOUT ME&lt;/h2&gt;
            &lt;P&gt;나를 소개합니다.&lt;/P&gt;
            &lt;!-- 테이블 --&gt;
            &lt;!-- 
                table : 표 전체
                thead : 헤더 행 그룹
                tbody : 본문 행 그룹
                tr : 행(row)
                th : 헤더 셀(굵게 + 가운데 정렬)
                td : 일반 셀
            --&gt;
            &lt;table border=&quot;1&quot;&gt;
                &lt;tr&gt;
                    &lt;th&gt;이름&lt;/th&gt;
                    &lt;td&gt;서지현&lt;/td&gt;
                &lt;/tr&gt;
                &lt;tr&gt;
                    &lt;th&gt;MBTI&lt;/th&gt;
                    &lt;td&gt;INFP&lt;/td&gt;
                &lt;/tr&gt;
                &lt;tr&gt;
                    &lt;th&gt;사는 곳&lt;/th&gt;
                    &lt;td&gt;인천&lt;/td&gt;
                &lt;/tr&gt;
            &lt;/table&gt;

        &lt;/section&gt;

        &lt;section id=&quot;favorites&quot;&gt;
            &lt;h2&gt;FAVORITES&lt;/h2&gt;
            &lt;P&gt;내가 좋아하는 것들&lt;/P&gt;

            &lt;!-- dl dt dd --&gt;
            &lt;dl&gt;
                &lt;dt&gt;1. 내가 제일 좋아하는 음식은?&lt;/dt&gt;
                &lt;dd&gt;피자&lt;/dd&gt;
                &lt;dt&gt;2. 최애 영화 혹은 드라마는?&lt;/dt&gt;
                &lt;dd&gt;선재업고튀어, 미지의서울, 그해우리는 등&lt;/dd&gt;
                &lt;dt&gt;3. 취미는?&lt;/dt&gt;
                &lt;dd&gt;재봉틀, 걷기&lt;/dd&gt;
            &lt;/dl&gt;

        &lt;/section&gt;


        &lt;section id=&quot;experience&quot;&gt;
            &lt;h2&gt;EXPERIENCE&lt;/h2&gt;
            &lt;!-- 이미지 + 캡션 --&gt;
            &lt;figure&gt;
                &lt;img src=&quot;#&quot; alt=&quot;산티아고 순례길&quot; width=&quot;500&quot;&gt;
                &lt;figcaption&gt;사진 1. 산티아고 순례길&lt;/figcaption&gt;
            &lt;/figure&gt;
        &lt;/section&gt;

        &lt;section id=&quot;guestbook&quot;&gt;
            &lt;h2&gt;GUEST BOOK&lt;/h2&gt;
            &lt;!-- 
                form : 데이터를 사용자에게 입력 받아 서버에 전송하는 컨테이너
                action : 데이터를 보낼 URL
                method : GET(URL에 노출) VS POST(숨겨서 전송)
            --&gt;
            &lt;form action=&quot;#&quot; method=&quot;post&quot;&gt;
                &lt;!-- fieldset : 연관된 입력 요소를 그룹화--&gt;
                &lt;fieldset&gt;
                    &lt;label for=&quot;name&quot;&gt;이름&lt;/label&gt;
                    &lt;input type=&quot;text&quot; id=&quot;name&quot; name=&quot;name&quot; placeholder=&quot;이름을 입력해 주세요.&quot; required&gt;
                    &lt;br&gt;&lt;br&gt;

                    &lt;!-- textarea : 여러 줄 텍스트 입력 --&gt;
                    &lt;label for=&quot;message&quot;&gt;메세지&lt;/label&gt;&lt;br&gt;
                    &lt;textarea id=&quot;message&quot; name=&quot;message&quot; rows=&quot;5&quot; cols=&quot;40&quot; placeholder=&quot;메세지를 입력해 주세요.&quot;&gt;&lt;/textarea&gt;

                    &lt;!-- type = &quot;submit&quot; : 데이터 전송 --&gt;
                    &lt;button type=&quot;submit&quot;&gt;전송&lt;/button&gt;
                    &lt;!-- type = &quot;reset&quot; : 모든 입력 값 초기화 --&gt;
                    &lt;button type=&quot;reset&quot;&gt;초기화&lt;/button&gt;
                &lt;/fieldset&gt;
            &lt;/form&gt;
        &lt;/section&gt;

    &lt;/main&gt;

    &lt;!-- footer : 사이트 하단 영역 --&gt;
    &lt;footer&gt;
        &lt;p&gt;&amp;copy; 2026. 서지현. All rights reserved.&lt;/p&gt;
    &lt;/footer&gt;

&lt;/body&gt;
&lt;/html&gt;</code></pre>]]></description>
        </item>
    </channel>
</rss>