9:20 경 학원 도착
<9:30 1교시>
웹 서버를 서비스하기
고정된 ip에 연결되어 있는 어떤 서버 컴퓨터에다가 이 웹서버를 돌려야함
이 서버 컴퓨터는 대부분 윈도우나 리눅스 운영체제를 사용하는데 대부분 리눅스임.
왜 리눅스를 쓰느냐...는 오픈 소스 운영체제여서 무료라서...ㅋㅋㅋ
우리는 윈도우 환경에서 개발했는데 리눅스에서 잘 돌아갈까?
App.java를 컴파일 하면 App.class가 나오고,
이 App.class를 윈도우나 리눅스나 맥os나 각각의 운영체제에 존재하는 java.exe 들이 실행시켜주는데,
윈도우용 자바 해석 컴파일러와 리눅스용 자바 해석 컴파일러가 서로 다르긴하지만 java이기 때문에 하나의 코드로 플랫폼(운영체제)에 상관없이 다 실행이 가능하다. (one source multi platform)
정리하자면 윈도우에서 개발한 java 코드던, 리눅스에서 개발한 java 코드던, 맥OS에서 개발한 java 코드던 다 실행이 됨.
cf) C언어로 만들어진 실행 파일은 윈도우에서 만든건 윈도우에서만, 리눅스에서 만든건 리눅스에서만, 맥에서 만든건 맥에서만 돌아감.
윈도우나 리눅스에서 실행할 방법(실제 서버에서는 이클립스를 사용하지 않음)
1. 자바 설치
2. 아파치 톰캣을 리눅스의 특정 서버에 가져다 놓기
톰캣을 서비스할 재료는 다이나믹 웹 프로젝트인데 이걸 컴파일한 .class 파일만 압축한 .war 파일이란 결과물을 얻어내서
톰캣 서버의 특정 폴더에 넣고 톰캣서버를 실행하는 것이다.
.war 파일을 얻는 방법은 프로젝트를 우클릭하여 export 할 때 .war로
1. 바탕화면에 가상의 리눅스 서버 만들기
2. 리눅스 서버에 자바 설치하기(우리는 컴퓨터에 설치된 상태)
자바를 설치한다는건 cmd 창에서 java를 누르면 관련 프로그램 내용이 쭉 뜬다.
3. Linux Server (폴더)에 톰캣 복사해서 가져다 놓기
4.이클립스에서 war 파일 얻기 :프로젝트 우클릭 >export>경로 지정
5. 아파치 폴더에서 탐색
bin : 실행파일이 있는 곳
conf : xxx.xml 문서와 같은 설정 파일이 있는 곳 -> 포트 번호를 바꾸고, context.xml에서 설정하기
webapps: war파일을 넣을 곳
톰캣 서버가 서비스 하게 만드는 방법
결과물을 webapps에 넣어놓고, conf에서 포트번호 DB연결 설정하고, bin에 들어가서 톰캣 서버 실행하면 서버가 돌아가면서 war을 압축해제 하면서 알아서 돌리는 거임. 대박.
이클립스는 이 과정을 대신 해줬음.
7.conf의 설정 바꾸기 : server.xml의 port 80으로 바꾸고 이클립스에서 연결한 DB 설정 복사해오기
8. War 파일을 webapp에 넣기(war 파일 제외 나머지는 삭제해도 됨)
9.bin 폴더 가기 > 우클릭으로 터미널에서 열기 입력
startup과 shutdown이 있는데, 윈도우에서는 이걸 이용해서 실행하고 끄고, 리눅스에서는 startup.sh나 shutdown.sh를 사용해서 실행하고 끔.
10. bin>./startup을 누르면 경고창 뜨는거 허용하면 Tomcat 서버의 실행창이 열리면서 war가 압축해제돼서 우리가 만들었던 프로젝트 파일이 나타남
우리가 이클립스에서 만들었던 것들이 들어가 있다.
11. localhost:80/Step07Final을 주소창에 입력하면 페이지가 실행됨.
:80은 써도 되고 안써도 됨, 기본 포트가 80 포트여서 localhost/Step07Final 이라고 알아서 뜸.
12. 서버의 설정으로 클라이언트에게는 프로젝트 명을 숨기고 이름을 바꿀 수 있다. 그럴려면 서버가 꺼진 상태여야 한다.
bin> ./shutdown
vscode에서 server.xml에 빨간 밑줄 쓴 부분 쓰기
Context path=""가 비어 있어서 Context 경로가 사라짐. 그래서 localhost만 입력해도 됨. 신기.
그럼 따옴표 안에다가 index.html 쓰면 index.html로 뜨나? 궁금스. 다음시간에 알려주시겠지?
는 프로젝트 준비하라고 시간주심.
updateform 유효성 검사랑 버튼 활성화가 풀림이 오류 있어서 다시 해서 올림.
<%@page import="test.dao.Com1CeoDao"%>
<%@page import="test.dto.Com1CeoDto"%>
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%
int empno=(int)session.getAttribute("empno");
//Com1CeoDao dao = Com1CeoDao.getInstance();
//Com1CeoDto dto = dao.getData(empno);
Com1CeoDto dto = Com1CeoDao.getInstance().getData(empno);
// 로그인한 사용자의 정보를 DB에서 가져오기 (예시)
//Com1CeoDto ceoInfo = Com1CeoDao.getInstance().getData(empno); // 사원번호를 이용하여 CEO 정보를 조회
%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>/ceo/protected/updateform_ceo</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" />
<style>
.container2 {
max-width: 800px;
margin: 40px auto;
background-color: #fff;
padding: 20px;
border-radius: 8px;
border: 1px solid black;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
</style>
</head>
<body>
<%@ include file="/include/header.jsp" %>
<%-- 관리자 페이지 전용 네비바: 관리자 페이지 이동을 쉽게 하기 위함 --%>
<jsp:include page="/include/navbar.jsp"></jsp:include>
<div class="container2" id="app">
<h3>회원 정보 수정 양식</h3>
<form action="update.jsp" method="get" id="callupdateForm" @submit.prevent="onSubmit" novalidate>
<div class="mb-2">
<label class="form-label" for="ename">이름</label>
<input v-model="ename" :class="{'is-valid': isEnameValid && isEnameDirty, 'is-invalid': !isEnameValid && isEnameDirty}"
@input="onEnameInput" class="form-control" type="text" name="ename" id="ename" value="<%=dto.geteName() %>" required/>
<div class="invalid-feedback">이름을 올바르게 입력하세요.</div>
</div>
<div class="mb-2">
<label class="form-label" for="ecall">연락처</label>
<input class="form-control" @input="onEcallInput" :class="{'is-invalid': !isEcallValid && isEcallDirty, 'is-valid':isEcallValid}"
type="text" name="ecall" id="ecall" v-model="ecall" value="<%=dto.geteCall() %>" required/>
<small class="form-text">하이픈(-)을 포함하여 기재해주세요.</small>
<div class="invalid-feedback">전화번호 형식에 맞지 않습니다.</div>
</div>
<div class="mb-2">
<label class="form-label" for="password">기존 비밀번호</label>
<input class="form-control" @input="onPwdInput"
:class="{
'is-invalid':!isOriginPwdMatch && isOriginPwdMatchDirty,
'is-valid': isOriginPwdMatchDirty && isOriginPwdMatch
}"
type="password" name="password" id="password" v-model="password" required/>
<div class="invalid-feedback" v-if="!isOriginPwdMatch && isOriginPwdMatchDirty">비밀번호가 일치하지 않습니다.</div>
</div>
<div class="mb-2">
<label class="form-label" for="newPassword">새 비밀번호 (선택사항)</label>
<input class="form-control" type="password" name="newPassword" id="newPassword"
@input="onNewPwdInput" v-model="newPassword"
:class="{
'is-invalid': (!isNewPwdValid && isNewPwdDirty) || (isSameOriginPwd && isSameOriginPwdDirty),
'is-valid': isNewPwdValid && !isSameOriginPwd
}"/>
<small class="form-text">영문자, 숫자, 특수문자를 포함하여 최소 8자리 이상 입력하세요.</small>
<div class="invalid-feedback" v-if="!isNewPwdValid && isNewPwdDirty">비밀번호 형식이 올바르지 않습니다.</div>
<div class="invalid-feedback" v-if="isSameOriginPwd && isSameOriginPwdDirty">기존 비밀번호와 같습니다.</div>
</div>
<div class="mb-2">
<label class="form-label" for="newPassword2">새 비밀번호 확인</label>
<input class="form-control" type="password" id="newPassword2"
@input="onNewPwdConfirmInput" v-model="newPassword2"
:class="{'is-invalid': !isNewPwdMatch && isNewPwdMatchDirty, 'is-valid': isNewPwdMatch && isNewPwdMatchDirty}" />
<div class="invalid-feedback">비밀번호가 일치하지 않습니다.</div>
</div>
<button class="btn btn-success" type="submit" :disabled="!(isOriginPwdMatch&&((isEcallValid&isEcallDirty)||(isEnameValid&&isEnameDirty)||(isNewPwdMatchDirty&&isNewPwdMatch)))">수정하기</button>
</form>
</div>
<div class="position-fixed bottom-0 w-100">
<%@ include file="/include/footer.jsp" %>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script>
new Vue({
el:"#app",
data:{
ename:"<%=dto.geteName()%>",
ecall:"<%=dto.geteCall()%>",
password: "",
isPwdValid:false,
isNewPwdValid:false,
isEcallValid:false,
isEnameValid: true,
isEnameDirty: false,
newPassword:"",
newPassword2:"",
isNewPwdMatch: false,
isNewPwdMatchDirty: false,
isEcallDirty:false,
isPwdDirty:false, // 비밀번호 입력란에 한 번이라도 입력했는지 여부
isNewPwdDirty:false, // 새 비밀번호 입력란에 한 번이라도 입력했는지 여부
isSameOriginPwd: false, // 기존 비밀번호와 새 비밀번호 비교
isSameOriginPwdDirty: false,
isOriginPwdMatch: false,
isOriginPwdMatchDirty: false
},
methods:{
onEnameInput(e) {
this.isEnameValid = false;
this.ename = e.target.value;
const reg_ename = /^[가-힣]{2,5}$/;
this.isEnameDirty = true;
if(this.isEnameDirty){
this.isEnameValid = reg_ename.test(this.ename);
}
},
onEcallInput(){
//현재까지 입력한 비밀번호
//공백이 아닌 한글자가 한번이상 반복 되어야 통과 되는 정규표현식
const reg_ecall=/^01[016789]-\d{3,4}-\d{4}$/;
this.isEcallDirty = true;
this.isEcallValid = reg_ecall.test(this.ecall);
if (this.isEcallValid) {
fetch("../../../checkEcall", {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: 'ecall=' + encodeURIComponent(this.ecall) + '&role=' + encodeURIComponent('CEO')
})
.then(res => res.json())
.then(data => {
if (data.isDuplicate) {
alert('이미 등록된 전화번호입니다.');
this.isEcallValid = false;
this.ecall = "";
} else {
alert('사용 가능한 전화번호입니다.');
}
})
.catch(error => {
alert('에러 발생: ' + error);
});
}
},
onPwdInput(e) {
const enteredPwd = e.target.value;
//const reg_pwd = /^(?=.*[A-Za-z])(?=.*\d)(?=.*[\W_]).{8,}$/;
//this.isPwdDirty = true;
//this.isPwdValid = reg_pwd.test(enteredPwd);
// 기존 비밀번호와 입력한 비밀번호 비교
this.isOriginPwdMatchDirty = true;
this.isOriginPwdMatch = (enteredPwd =="<%=dto.getePwd()%>");
},
onNewPwdInput() {
const reg_pwd = /^(?=.*[A-Za-z])(?=.*\d)(?=.*[\W_]).{8,}$/;
this.isNewPwdDirty = true;
this.isNewPwdValid = reg_pwd.test(this.newPassword);
this.isSameOriginPwdDirty = true;
this.isSameOriginPwd = this.newPassword === this.password;
},
onNewPwdConfirmInput() {
this.isNewPwdMatch = this.newPassword === this.newPassword2 && this.newPassword2.trim() !== "";
this.isNewPwdMatchDirty = true;
},
onSubmit(event) {
if (this.password==this.newPassword){
alert("새 비밀번호가 기존 비밀번호와 같습니다.")
event.preventDefault();
return;
}
if(this.isNewPwdDirty && this.isNewPwdMatchDirty){
if (this.newPassword != this.newPassword2) {
alert("비밀번호 확인란이 입력되지 않았습니다.");
event.preventDefault();
return;
}
if (!this.isNewPwdMatch) {
alert("비밀번호가 일치하지 않습니다.");
event.preventDefault();
return;
}
}
document.getElementById("callupdateForm").submit();
}
}});
</script>
</body>
</html>
1교시에 했던거 궁금한거 그냥 내가 해보기로 했음.
음 안된다. 샘께 질문해야지.
이렇게 쓰면 말도 안되는거라고 혼남....ㅋㅋㅋㅋㅋㅋ
내가 경로를 지정할 수 있는 기능이라는거임.
그래서 이런게 된다고 하셨음.
이렇게하면 웹페이지에서 나타날 때, http://localhost:80/acorn이라고 나타나고
index.jsp는 자동으로 불러와지는거라고 하심
새 책(스프링부트) 받음.
스프링부트는 프로젝트 끝나고 월요일부터 나간다고 함.
스프링부트(백엔드) 하면
- 요청 파라미터가 자동으로 추출해서 다 담아준다고 함.
- 글구 데이터 타입도 알아서 담아준다고 함.
- dao가 필요하면 dao 선언만 해놓으면 참조값이 알아서 들어와서 스프링으로부터 주입(?)받아서 쓸 수 있다고 함.
- 의존 객체를 주입해준다고 함.
- jsp 페이지를 사용하지 않음 ->타임리프 구조(심지어 이 이후에는 json만 응답하면 되게 한다고 함)
- 스프링 시큐리티로 보안 인증 시스템이 적용된 로그인 코딩
그 이후엔 리액트(프론트엔드)배우고 -> 페이지가 깜빡거림 없이 화면을 렌더링 함.
그리고 끝.
스프링부트는 어렵진 않은데 리액트는 겁나 어렵다고 함.
시간되면 타입 스크립트 알려주신다고 하는데 타입 스크립트는 데이터 타입을 엄격하게 지키는 거라 생각하면 됨.
프로젝트 일단 마무리돼서 프로젝트는 깃허브로 비공개로 올려둘거고,
내가 했던 부분은 백업은 따로 글로 올릴거고..
내가 다른 사람 아이디어 줬던것도 관련해서 일단은 올리려고 함.
비공개로 올려둠.
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%
session.setAttribute("current_page", "");
%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css">
<style>
.container3 {
text-align: center;
}
button {
width: 500px; /* 버튼의 고정된 너비 */
height: 80px; /* 버튼의 고정된 높이 */
font-size: 30px; /* 글자 크기 */
padding: 20px; /* 버튼 내부 여백 */
margin: 5px; /* 버튼 사이 간격 */
cursor: pointer; /* 마우스 커서 변경 */
border: none; /* 기본 버튼 테두리 없애기 */
background-color: grey; /* 버튼 배경 색 */
color: white; /* 버튼 텍스트 색 */
border-radius: 5px; /* 버튼 모서리 둥글게 */
transition: background-color 0.3s; /* 버튼 색 변경에 대한 애니메이션 */
}
button:hover {
background-color:black; /* 호버 시 색상 변화 */
}
.container2 {
max-width: 800px;
margin: 40px auto;
background-color: #fff;
padding: 20px;
border-radius: 8px;
border: 1px solid black;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
</style>
</head>
<body class="d-flex flex-column min-vh-100 bg-light">
<%@ include file="/include/header.jsp" %>
<%-- 관리자 페이지 전용 네비바 --%>
<jsp:include page="/include/navbar.jsp"></jsp:include>
<div class="main flex-grow-1">
<div class="container2">
<div class="container3">
<p>
<a href="info/view.jsp" style="text-decoration: none; color: inherit;">
<button>나의 정보 보기</button>
</a>
</p>
</div>
<div class="container3">
<p>
<a href="sale/view.jsp" style="text-decoration: none; color: inherit;">
<button>매출 관리</button>
</a>
</p>
</div>
<div class="container3">
<p>
<a href="accept/acceptForm.jsp" style="text-decoration: none; color: inherit;">
<button>가입 승인</button>
</a>
</p>
</div>
<div class="container3">
<p>
<a href="employee/manageForm.jsp" style="text-decoration: none; color: inherit;">
<button>직원 관리</button>
</a>
</p>
</div>
<div class="container3">
<p>
<a href="quit/quitForm.jsp" style="text-decoration: none; color: inherit;">
<button>퇴사자 관리</button>
</a>
</p>
</div>
</div>
</div>
<%@ include file="/include/footer.jsp" %>
</body>
</html>
<%@page import="test.dto.Com1Dto"%>
<%@page import="test.dao.Com1SaleDao"%>
<%@page import="test.dao.Com1Dao"%>
<%@page import="test.dto.Com1SaleDto"%>
<%@page import="java.util.List"%>
<%@page import="java.util.ArrayList" %>
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%
//현재 페이지 위치를 세션 영역에 저장 (관리자 전용 네비바에 활성 상태 표시 위함)
session.setAttribute("current_page", "view");
//로그인 상태 표시 : 세션 영역에서 접속 계정 정보 가져오기
Com1SaleDao saledao = Com1SaleDao.getInstance();
List<Integer> storenums = Com1Dao.getInstance().getStoreNumList();
List<Com1SaleDto> listall= saledao.getListAll();
List<Com1SaleDto> listmonth = saledao.getListSalebyMonth();
List<Com1SaleDto> listyear = saledao.getListSalebyYear();
List<Com1SaleDto> listbystore = new ArrayList<>();
List<Com1SaleDto> listbystoremonthly = new ArrayList<>();
List<Com1SaleDto> listbystoreyearly = new ArrayList<>();
int storenum = -1;
String strStoreNum=request.getParameter("storenum");
if(strStoreNum!=null&&!strStoreNum.isEmpty()){
storenum = Integer.parseInt(strStoreNum);
listbystore = saledao.getListbyStore(storenum);
listbystoremonthly = saledao.getListMonthlybyStore(storenum);
listbystoreyearly= saledao.getListYearlybyStore(storenum);
};
request.setAttribute("storenum", storenum);
/* String storecall=request.getParameter("storecall");
if(storecall!=null&&storecall.isEmpty()){
Com1Dto dto = new Com1Dto();
Com1Dao.getInstance().insert(storecall);
};
request.setAttribute("storecall",storecall); */
%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>sale/view.jsp</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" />
<style>
.table-container {
padding-bottom: 100px; /* footer 높이보다 여유 있게 추가 */
}
</style>
<!-- 페이지 로딩에 필요한 자원 -->
</head>
<body class="d-flex flex-column min-vh-100 bg-light">
<%@ include file="/include/header.jsp" %>
<%-- 관리자 페이지 전용 네비바 --%>
<jsp:include page="/include/navbar.jsp"></jsp:include>
<div class="main flex-grow-1">
<!-- 본문 -->
<div class="contents text-center mt-3 mx-auto" style="width: 900px;">
<!-- 조회조건 -->
<div class="nav nav-tabs">
<div>
<button class="tab-button nav-item nav-link" id="allTab" onclick="switchTab('all')">전체 매출</button>
</div>
<div>
<button class="tab-button nav-item nav-link" id="yearTab" onclick="switchTab('year')">전체 연매출</button>
</div>
<div>
<button class="tab-button nav-item nav-link" id="monthTab" onclick="switchTab('month')">전체 월매출</button>
</div>
<div class="tab-button nav-item nav-link" id="storeTab" onclick="switchTab('store')">
<form action="view.jsp" method="get" id="storeForm">
<label for="storenum">지점 선택: </label>
<select name="storenum" id="storenum" onchange="switchTab('store'); document.getElementById('storeForm').submit();">
<option value="">-- 지점을 선택하세요 --</option>
<% for (Integer tmp : storenums) { %>
<option value="<%= tmp %>" <%= (tmp.equals(storenum)) ? "selected" : ""%> > <%= tmp %>호점
</option>
<% } %>
</select>
</form>
</div>
</div>
<div class="contents text-center mt-3 mx-auto" style="width: 900px;">
<div id="allContent" class="table-container tab-content p-3 bg-light rounded shadow-sm" style="display: block;">
<h5>전체 매장의 전체 일매출</h5>
<div class="table-responsive">
<table class="table table-hover text-center align-middle">
<thead class="table-dark">
<tr>
<th>호점</th>
<th>날짜 구분</th>
<th>일매출</th>
</tr>
</thead>
<tbody>
<% if (listall == null) { %>
<tr>
<td>매출 정보가 없습니다.</td>
</tr>
<%} else { %>
<%for (Com1SaleDto tmp : listall) { %>
<tr>
<td><%= tmp.getStoreNum() %></td>
<td><%= tmp.getSalesDate() %></td>
<td><%= tmp.getDailySales() %></td>
</tr>
<% }
}%>
</tbody>
</table>
</div>
</div>
<div id="yearContent" class="tab-content p-3 bg-light rounded shadow-sm" style="display: block;">
<h5>전체 매장의 전체 연매출</h5>
<div class="table-responsive">
<table class="table table-hover text-center align-middle">
<thead class="table-dark">
<tr>
<th>날짜 구분</th>
<th>연매출</th>
</tr>
</thead>
<tbody>
<% if ( listyear==null) { %>
<tr>
<td>연매출 정보가 없습니다.</td>
</tr>
<% } else { %>
<% for (Com1SaleDto tmp : listyear) { %>
<tr>
<td><%= tmp.getSyear() %></td>
<td><%= tmp.getYearlySales() %></td>
</tr>
<% }
}%>
</tbody>
</table>
</div>
</div>
<div id="monthContent" class="tab-content p-3 bg-light rounded shadow-sm" style="display: block;">
<h5>전체 매장의 전체 월매출</h5>
<div class="table-responsive">
<table class="table table-hover text-center align-middle">
<thead class="table-dark">
<tr>
<th>날짜 구분</th>
<th>월매출</th>
</tr>
</thead>
<tbody>
<% if (listmonth==null) { %>
<tr>
<td>월매출 정보가 없습니다.</td>
</tr>
<%}else{ %>
<%for (Com1SaleDto tmp: listmonth) { %>
<tr>
<td><%= tmp.getSmonth() %></td>
<td><%= tmp.getMonthlySales() %></td>
</tr>
<% }
}%>
</tbody>
</table>
</div>
</div>
<div id="storeContent" class="table-container tab-content p-3 bg-light rounded shadow-sm" style="display: block;">
<h5>현 매장의 전체 연매출</h5>
<div class="table-responsive">
<table class="table table-hover text-center align-middle">
<thead class="table-dark">
<tr>
<th class="col-5">날짜 구분</th>
<th class="col-4">연매출</th>
</tr>
</thead>
<tbody>
<% if (listbystoreyearly.size()==0) { %>
<tr>
<td colspan="2">매장을 선택하세요!</td>
</tr>
<%}else{ %>
<%for (Com1SaleDto tmp: listbystoreyearly) { %>
<tr>
<td><%= tmp.getSyear() %></td>
<td><%= tmp.getYearlySales() %></td>
</tr>
<% }
}%>
</tbody>
</table>
</div>
<h5>현 매장의 전체 월매출</h5>
<div class="table-responsive">
<table class="table table-hover text-center align-middle">
<thead class="table-dark">
<tr>
<th class="col-5">날짜 구분</th>
<th class="col-4">월매출</th>
</tr>
</thead>
<tbody>
<% if (listbystoremonthly.size()==0) { %>
<tr>
<td colspan="2">매장을 선택하세요!</td>
</tr>
<%}else{ %>
<%for (Com1SaleDto tmp: listbystoremonthly) { %>
<tr>
<td><%= tmp.getSmonth() %></td>
<td><%= tmp.getMonthlySales() %></td>
</tr>
<% }
}%>
</tbody>
</table>
</div>
<h5>현 매장의 전체 일매출</h5>
<div class="table-responsive">
<table class="table table-hover text-center align-middle">
<thead class="table-dark">
<tr>
<th class="col-5">날짜 구분</th>
<th class="col-4">일매출</th>
</tr>
</thead>
<% if (listbystore.size()==0){ %>
<tbody>
<tr>
<td colspan="2">매장을 선택하세요!</td>
</tr>
</tbody>
<%}else{ %>
<tbody>
<%for (Com1SaleDto tmp: listbystore) { %>
<tr>
<td><%= tmp.getSdate()%></td>
<td><%= tmp.getDailySales()%></td>
</tr>
<% }%>
</tbody>
<%}%>
</table>
</div>
</div>
</div>
</div>
<div class="table-container">
</div>
</div>
<script>
function switchTab(tab) {
event.preventDefault();
const tabs = ['all', 'year', 'month', 'store'];
tabs.forEach(t => {
document.getElementById(t + 'Content').style.display = 'none';
document.getElementById(t + 'Tab').classList.remove('active-tab');
});
document.getElementById(tab + 'Content').style.display = 'block';
document.getElementById(tab + 'Tab').classList.add('active-tab');
}
window.onload = function() {
const tabs = ['all', 'year', 'month', 'store'];
tabs.forEach(t => {
document.getElementById(t + 'Content').style.display = 'none';
document.getElementById(t + 'Tab').classList.remove('active-tab');
});
const urlParams = new URLSearchParams(window.location.search);
if (urlParams.has('storenum')) {
switchTab('store');
}
};
</script>
<%@ include file="/include/footer.jsp" %>
</body>
</html>
<%@page import="test.dto.UsingDto"%>
<%@page import="test.dao.UsingDao"%>
<%@page import="test.dao.Com1CeoDao"%>
<%@page import="test.dto.Com1CeoDto"%>
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%
//현재 페이지 위치를 세션 영역에 저장 (관리자 전용 네비바에 활성 상태 표시 위함)
session.setAttribute("current_page", "myinfo");
int empno=(int)session.getAttribute("empno");
String comname=(String)session.getAttribute("comname");
Com1CeoDto dto = Com1CeoDao.getInstance().getData(empno);
%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>/ceo/myinfo_ceo.jsp</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" />
<style>
table {
width: 100%; /* 화면 너비에 맞게 설정 */
border-spacing: 0;
border-collapse: collapse; /* 테이블의 경계를 합칩니다 */
margin-bottom: 50px;
}
th, td {
padding: 20px; /* 셀 안의 여백을 일정하게 */
text-align: left; /* 텍스트를 왼쪽 정렬 */
border-bottom: 1px solid #ddd; /* 셀에 테두리 추가 */
}
th {
background-color: #f4f4f4; /* 헤더 셀 배경색 */
text-align: right; /* 텍스트를 오른쪽 정렬 */
width: 25%; /* 각 열의 넓이를 고정 비율로 설정 */
}
td {
width: 75%; /* 데이터 셀의 넓이를 고정 비율로 설정 */
}
/* 페이지에 맞게 조정된 폰트 크기 */
h1 {
margin-bottom: 30px;
}
/* 링크 스타일 수정 */
a {
color: #007bff;
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
.container2 {
max-width: 800px;
margin: 40px auto;
background-color: #fff;
padding: 20px;
border-radius: 8px;
border: 1px solid black;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
h1 {
text-align: center;
margin-bottom: 20px;
/* color: #333; */
}
table {
width: 100%;
border-collapse: collapse;
margin: 20px 0;
}
th, td {
padding: 10px;
text-align: left;
border-bottom: 1px solid #ddd;
}
th {
font-weight: bold;
}
td a {
color: #007bff;
text-decoration: none;
font-weight: bold;
}
td a:hover {
text-decoration: underline;
}
.btn-container {
display: flex;
justify-content: center;
margin-top: 20px;
}
.btn-container a {
margin: 0 10px;
padding: 10px 20px;
border-radius: 4px;
text-decoration: none;
font-weight: bold;
background-color: #007bff;
color: #fff;
transition: background-color 0.3s;
}
.btn-container a:hover {
background-color: #0056b3;
}
</style>
</head>
<body class="d-flex flex-column min-vh-100 bg-light">
<%@ include file="/include/header.jsp" %>
<%-- 관리자 페이지 전용 네비바: 관리자 페이지 이동을 쉽게 하기 위함 --%>
<jsp:include page="/include/navbar.jsp"></jsp:include>
<div class="main flex-grow-1">
<div class="container2 ">
<div
style="display: flex; justify-content: space-between; align-items: center;">
<h1>개인정보</h1>
<a id="updatemyinfo" href="updateform.jsp"
class="btn btn-primary btn-m">개인정보수정</a>
</div>
<table>
<tr>
<th>회사</th>
<td>${comname }(회사 코드 : <%=dto.getComId()%>)</td>
</tr>
<tr>
<th>관리자 사원번호</th>
<td><%=dto.getEmpNo()%></td>
</tr>
<tr>
<th>관리자 이름</th>
<td><%=dto.geteName()%></td>
</tr>
<tr>
<th>직급</th>
<td><%=dto.getRole()%></td>
</tr>
<tr>
<th>연락처</th>
<td><%=dto.geteCall()%></td>
</tr>
<tr>
<th>비밀번호</th>
<td><%=dto.getePwd()%></td>
</tr>
</table>
<div class="btn-container">
<a href="${pageContext.request.contextPath }/companyone/ceo/ceoMain.jsp">메인 페이지로</a>
</div>
</div>
</div>
<%@ include file="/include/footer.jsp" %>
</body>
</html>
<%@page import="test.dao.Com1CeoDao"%>
<%@page import="test.dto.Com1CeoDto"%>
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%
int empno=(int)session.getAttribute("empno");
//Com1CeoDao dao = Com1CeoDao.getInstance();
//Com1CeoDto dto = dao.getData(empno);
Com1CeoDto dto = Com1CeoDao.getInstance().getData(empno);
// 로그인한 사용자의 정보를 DB에서 가져오기 (예시)
//Com1CeoDto ceoInfo = Com1CeoDao.getInstance().getData(empno); // 사원번호를 이용하여 CEO 정보를 조회
%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>/ceo/protected/updateform_ceo</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" />
<style>
.container2 {
max-width: 800px;
margin: 40px auto;
background-color: #fff;
padding: 20px;
border-radius: 8px;
border: 1px solid black;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
</style>
</head>
<body>
<%@ include file="/include/header.jsp" %>
<%-- 관리자 페이지 전용 네비바: 관리자 페이지 이동을 쉽게 하기 위함 --%>
<jsp:include page="/include/navbar.jsp"></jsp:include>
<div class="container2" id="app">
<h3>회원 정보 수정 양식</h3>
<form action="update.jsp" method="get" id="callupdateForm" @submit.prevent="onSubmit" novalidate>
<div class="mb-2">
<label class="form-label" for="ename">이름</label>
<input v-model="ename" :class="{'is-valid': isEnameValid && isEnameDirty, 'is-invalid': !isEnameValid && isEnameDirty}"
@input="onEnameInput" class="form-control" type="text" name="ename" id="ename" value="<%=dto.geteName() %>" required/>
<div class="invalid-feedback">이름을 올바르게 입력하세요.</div>
</div>
<div class="mb-2">
<label class="form-label" for="ecall">연락처</label>
<input class="form-control" @input="onEcallInput" :class="{'is-invalid': !isEcallValid && isEcallDirty, 'is-valid':isEcallValid}"
type="text" name="ecall" id="ecall" v-model="ecall" value="<%=dto.geteCall() %>" required/>
<small class="form-text">하이픈(-)을 포함하여 기재해주세요.</small>
<div class="invalid-feedback">전화번호 형식에 맞지 않습니다.</div>
</div>
<div class="mb-2">
<label class="form-label" for="password">기존 비밀번호</label>
<input class="form-control" @input="onPwdInput"
:class="{
'is-invalid':!isOriginPwdMatch && isOriginPwdMatchDirty,
'is-valid': isOriginPwdMatchDirty && isOriginPwdMatch
}"
type="password" name="password" id="password" v-model="password" required/>
<div class="invalid-feedback" v-if="!isOriginPwdMatch && isOriginPwdMatchDirty">비밀번호가 일치하지 않습니다.</div>
</div>
<div class="mb-2">
<label class="form-label" for="newPassword">새 비밀번호 (선택사항)</label>
<input class="form-control" type="password" name="newPassword" id="newPassword"
@input="onNewPwdInput" v-model="newPassword"
:class="{
'is-invalid': (!isNewPwdValid && isNewPwdDirty) || (isSameOriginPwd && isSameOriginPwdDirty),
'is-valid': isNewPwdValid && !isSameOriginPwd
}"/>
<small class="form-text">영문자, 숫자, 특수문자를 포함하여 최소 8자리 이상 입력하세요.</small>
<div class="invalid-feedback" v-if="!isNewPwdValid && isNewPwdDirty">비밀번호 형식이 올바르지 않습니다.</div>
<div class="invalid-feedback" v-if="isSameOriginPwd && isSameOriginPwdDirty">기존 비밀번호와 같습니다.</div>
</div>
<div class="mb-2">
<label class="form-label" for="newPassword2">새 비밀번호 확인</label>
<input class="form-control" type="password" id="newPassword2"
@input="onNewPwdConfirmInput" v-model="newPassword2"
:class="{'is-invalid': !isNewPwdMatch && isNewPwdMatchDirty, 'is-valid': isNewPwdMatch && isNewPwdMatchDirty}" />
<div class="invalid-feedback">비밀번호가 일치하지 않습니다.</div>
</div>
<button class="btn btn-success" type="submit" :disabled="!(isOriginPwdMatch&&((isEcallValid&isEcallDirty)||(isEnameValid&&isEnameDirty)||(isNewPwdMatchDirty&&isNewPwdMatch)))">수정하기</button>
</form>
</div>
<div class="position-fixed bottom-0 w-100">
<%@ include file="/include/footer.jsp" %>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script>
new Vue({
el:"#app",
data:{
ename:"<%=dto.geteName()%>",
ecall:"<%=dto.geteCall()%>",
password: "",
isPwdValid:false,
isNewPwdValid:false,
isEcallValid:false,
isEnameValid: true,
isEnameDirty: false,
newPassword:"",
newPassword2:"",
isNewPwdMatch: false,
isNewPwdMatchDirty: false,
isEcallDirty:false,
isPwdDirty:false, // 비밀번호 입력란에 한 번이라도 입력했는지 여부
isNewPwdDirty:false, // 새 비밀번호 입력란에 한 번이라도 입력했는지 여부
isSameOriginPwd: false, // 기존 비밀번호와 새 비밀번호 비교
isSameOriginPwdDirty: false,
isOriginPwdMatch: false,
isOriginPwdMatchDirty: false
},
methods:{
onEnameInput(e) {
this.isEnameValid = false;
this.ename = e.target.value;
const reg_ename = /^[가-힣]{2,5}$/;
this.isEnameDirty = true;
if(this.isEnameDirty){
this.isEnameValid = reg_ename.test(this.ename);
}
},
onEcallInput(){
//현재까지 입력한 비밀번호
//공백이 아닌 한글자가 한번이상 반복 되어야 통과 되는 정규표현식
const reg_ecall=/^01[016789]-\d{3,4}-\d{4}$/;
this.isEcallDirty = true;
this.isEcallValid = reg_ecall.test(this.ecall);
if (this.isEcallValid) {
fetch("../../../checkEcall", {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: 'ecall=' + encodeURIComponent(this.ecall) + '&role=' + encodeURIComponent('CEO')
})
.then(res => res.json())
.then(data => {
if (data.isDuplicate) {
alert('이미 등록된 전화번호입니다.');
this.isEcallValid = false;
this.ecall = "";
} else {
alert('사용 가능한 전화번호입니다.');
}
})
.catch(error => {
alert('에러 발생: ' + error);
});
}
},
onPwdInput(e) {
const enteredPwd = e.target.value;
//const reg_pwd = /^(?=.*[A-Za-z])(?=.*\d)(?=.*[\W_]).{8,}$/;
//this.isPwdDirty = true;
//this.isPwdValid = reg_pwd.test(enteredPwd);
// 기존 비밀번호와 입력한 비밀번호 비교
this.isOriginPwdMatchDirty = true;
this.isOriginPwdMatch = (enteredPwd =="<%=dto.getePwd()%>");
},
onNewPwdInput() {
const reg_pwd = /^(?=.*[A-Za-z])(?=.*\d)(?=.*[\W_]).{8,}$/;
this.isNewPwdDirty = true;
this.isNewPwdValid = reg_pwd.test(this.newPassword);
this.isSameOriginPwdDirty = true;
this.isSameOriginPwd = this.newPassword === this.password;
},
onNewPwdConfirmInput() {
this.isNewPwdMatch = this.newPassword === this.newPassword2 && this.newPassword2.trim() !== "";
this.isNewPwdMatchDirty = true;
},
onSubmit(event) {
if (this.password==this.newPassword){
alert("새 비밀번호가 기존 비밀번호와 같습니다.")
event.preventDefault();
return;
}
if(this.isNewPwdDirty && this.isNewPwdMatchDirty){
if (this.newPassword != this.newPassword2) {
alert("비밀번호 확인란이 입력되지 않았습니다.");
event.preventDefault();
return;
}
if (!this.isNewPwdMatch) {
alert("비밀번호가 일치하지 않습니다.");
event.preventDefault();
return;
}
}
document.getElementById("callupdateForm").submit();
}
}});
</script>
</body>
</html>
<%@page import="test.dto.Com1CeoDto"%>
<%@page import="test.dao.Com1CeoDao"%>
<%@ page language="java" contentType="text/html" pageEncoding="UTF-8"%>
<%
int empno = (int) session.getAttribute("empno");
Com1CeoDto dto = Com1CeoDao.getInstance().getData(empno);
String newName = request.getParameter("ename");
String newCall = request.getParameter("ecall");
String newPassword = request.getParameter("newPassword");
boolean isSuccess = false;
// Java의 문자열에선 \ = 이스케이프 문자
String reg_name = "^[가-힣]{2,5}$";
String reg_call = "^01[016789]-\\d{3,4}-\\d{4}$";
String reg_pwd = "^(?=.*[A-Za-z])(?=.*\\d)(?=.*[\\W_]).{8,}$";
// 이름 업데이트
if (newName != null && !newName.isEmpty()) {
if (newName.matches(reg_name)) {
dto.seteName(newName);
Com1CeoDao.getInstance().update(dto);
isSuccess = true;
} else {
%>
<script>
alert("이름 형식이 올바르지 않습니다.");
location.href = "updateform.jsp";
</script>
<%
return;
}
}
// 전화번호 업데이트
if (newCall != null && !newCall.isEmpty()) {
if (newCall.matches(reg_call)) {
dto.seteCall(newCall);
Com1CeoDao.getInstance().update(dto);
isSuccess = true;
} else {
%>
<script>
alert("전화번호 형식이 올바르지 않습니다.");
location.href = "updateform.jsp";
</script>
<%
return;
}
}
// 비밀번호 업데이트
if (newPassword != null && !newPassword.isEmpty()) {
if (newPassword.matches(reg_pwd)) {
dto.setePwd(newPassword);
Com1CeoDao.getInstance().update(dto);
isSuccess = true;
} else {
%>
<script>
alert("비밀번호 형식이 올바르지 않습니다.");
location.href = "updateform.jsp";
</script>
<%
return;
}
}
%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>수정 결과</title>
</head>
<body>
<div class="container">
<% if (isSuccess) { %>
<script>
alert("개인 정보를 수정했습니다.");
location.href = "${pageContext.request.contextPath }/companyone/ceo/info/view.jsp";
</script>
<% session.setAttribute("ename", newName);
} else { %>
<script>
alert("수정 실패했습니다.");
location.href = "updateform.jsp";
</script>
<% } %>
</div>
</body>
</html>
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%
//1.세션 초기화
session.invalidate();
//2. 응답
%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
<script>
alert("로그아웃 되었습니다");
//javascript 로 페이지 이동
location.href = "${pageContext.request.contextPath }/user/loginForm.jsp";
</script>
</body>
</html>
아이디어 줬던 코드.
다른 코드 다 잘 짜놓고 월급 입력하면 시급 입력 못하게 하는거랑 시급 입력하면 월급 입력 못하게 하는기능을 자꾸 유효성 검사 관련해서만 하려고 해서 input 요소를 비활성화 시키면 되잖아 라고 아이디어 줌.
<%@page import="test.dto.Com1EmpDto"%>
<%@page import="test.dao.Com1EmpDao"%>
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%
int empno = Integer.parseInt(request.getParameter("empno"));
String returnurl = request.getParameter("returnurl");
Com1EmpDao dao=Com1EmpDao.getInstance();
Com1EmpDto dto= dao.getData(empno);
%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>근무시간 수정폼</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" />
<style>
.containers {
max-width: 600px;
margin: 40px auto;
background-color: #fff;
padding: 20px;
border-radius: 8px;
border: 1px solid black;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.invalid-feedback{
display:none;
color: red;
}
</style>
</head>
<body class="d-flex flex-column min-vh-100 bg-light">
<%@ include file="/include/header.jsp" %>
<jsp:include page="/include/navbar.jsp"></jsp:include>
<%--main컨텐츠감싸기 --%>
<div class="main flex-grow-1">
<div class="containers" id ="app">
<h1>근무시간 급여변경</h1>
<form action="salUpdate.jsp?returnurl=<%=returnurl%>" method="post" id="myForm">
<div class="mb-3">
<label class="form-label">회사ID</label>
<input class="form-control" type="text" name="comid" value="<%=dto.getComId()%>" readonly />
</div>
<div class="mb-3">
<label class="form-label">소속지점</label>
<input class="form-control" type="text" name="storenum" value="<%=dto.getStoreNum()%>" readonly />
</div>
<div class="mb-3">
<label class="form-label">사원번호</label>
<input class="form-control" type="text" name="empno" value="<%=dto.getEmpNo()%>" readonly />
</div>
<div class="mb-3">
<label class="form-label">이름</label>
<input class="form-control" type="text" name="ename" value="<%=dto.geteName()%>" readonly />
</div>
<p style="color: green">알바는 시급과 근무시간만 기입 / 직원은 월급만 기입해주세요</p>
<div class="mb-3">
<label class="form-label">월급</label>
<input class="form-control" type="text" name="sal" id="sal" required oninput="salInput()"
v-model="sal" @input="validatesal"
:class="{'is-invalid': !issalValid && issalDirty, 'is-valid': issalValid}"/>
<div class="invalid-feedback">양수를 입력하세요.</div>
<div class="valid-feedback">직원은 월급만 입력해주세요.</div>
</div>
<div class="mb-3">
<label class="form-label">시급</label>
<input class="form-control" type="text" name="hsal" id="hsal" required oninput="hsalInput()"
v-model="hsal" @input="validatehsal"
:class="{'is-invalid': !ishsalValid && ishsalDirty, 'is-valid': ishsalValid}"/>
<div class="invalid-feedback">양수를 입력하세요.(시급과 근무시간을 함께 입력하세요)</div>
</div>
<div class="mb-3">
<label class="form-label">근무시간</label>
<input class="form-control" type="text" name="worktime" id="worktime" required oninput="hsalInput()"
v-model="worktime" @input="validateworktime"
:class="{'is-invalid': !isworktimeValid && isworktimeDirty, 'is-valid': isworktimeValid}"/>
<div class="invalid-feedback">양수를 입력하세요.(시급과 근무시간을 함께 입력하세요)</div>
</div>
<div class="d-flex justify-content-between">
<button class="btn btn-success" type="submit" id="subBtn"
:disabled="!(issalValid && ishsalValid && isworktimeValid)"
>수정하기</button>
<a href="<%=returnurl%>" class="btn btn-danger btn-block mb-2">돌아가기</a>
</div>
</form>
</div>
</div> <%--메인 --%>
<%--푸터 --%>
<jsp:include page="/include/footer.jsp" />
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script>
new Vue({
el: "#app",
data: {
sal: "<%=dto.getSal()%>",
hsal: "<%=dto.getHsal()%>",
worktime: "<%=dto.getWorktime()%>",
issalDirty: false,
issalValid: false,
ishsalValid: false,
ishsalDirty: false,
isworktimeValid: false,
isworktimeDirty: false,
},
methods: {
validatesal() {
this.hsal= "0";
this.worktime= "0";
this.ishsalValid = true;
this.isworktimeValid = true;
this.issalDirty = true;
const reg= /^[1-9]\d*$/;
this.issalValid = reg.test(this.sal);
},
validatehsal() {
this.sal= "0";
this.ishsalDirty = true;
const reg= /^[1-9]\d*$/;
this.ishsalValid = reg.test(this.hsal);
this.isworktimeValid = reg.test(this.worktime);
if (this.ishsalValid || this.isworktimeValid) {
this.issalValid = true;
}
},
validateworktime() {
this.sal= "0";
this.isworktimeDirty = true;
const reg= /^[1-9]\d*$/;
this.isworktimeValid = reg.test(this.worktime);
this.ishsalValid = reg.test(this.hsal);
if (this.ishsalValid && this.isworktimeValid) {
this.issalValid = true;
}
/*else {
this.issalValid = false;
}*/
}
}
});
//뷰
/*
function salInput() {
let sal = document.getElementById("sal");
let hsal = document.getElementById("hsal");
let worktime = document.getElementById("worktime");
if (sal.value.trim() !== "" ) {
hsal.setAttribute("readonly","");
worktime.setAttribute("readonly","");
hsal.value = "0";
worktime.value = "0";
} else {
hsal.removeAttribute("readonly");
worktime.removeAttribute("readonly");
}
}
function hsalInput() {
let sal = document.getElementById("sal");
let hsal = document.getElementById("hsal");
let worktime = document.getElementById("worktime");
if ( hsal.value.trim() !== "" || worktime.value.trim() !== "" ) {
sal.setAttribute("readonly","");
sal.value = "0";
}
else {
sal.removeAttribute("readonly");
}
}
*/
</script>
</body>
</html>
남은 시간은 새로 받은 책 읽어보다가 가야징
'자바풀스택 과정 > 자바 풀 스택 : 수업내용정리' 카테고리의 다른 글
자바 풀 스택 2/17 하루 기록 056 (0) | 2025.02.17 |
---|---|
자바 풀 스택 2/14 하루 기록 055 (0) | 2025.02.14 |
자바 풀 스택 2/12 하루 기록 053 (0) | 2025.02.12 |
자바 풀 스택 2/11 하루 기록 052 (0) | 2025.02.11 |
자바 풀 스택 2/10 하루 기록 051 (0) | 2025.02.10 |