전체 글 (135)

728x90


1차 일지 - [ 설정 ], [ 회원가입 ]
2차 일지 - [ 로그인 ]
3차 일지 - [ index에서 로그인 상태에 따라 display none/block 제어하고 p태그 내용 바꾸기 ], [ 로그아웃 ]
4차 일지 - [ header에 회원 아이디, 회원정보/로그아웃 링크 or 비회원 문구 추가 후 로그인 상태에 따라 display none/block 제어 ], [ 일반 회원 정보 확인 ]
5차 일지 - [ 일반 회원 정보 출력 변경 ], [ 관리자) 회원 정보 목록 출력 ], [ 일반 회원 비밀번호 변경 ]
6차 일지 - [ 게시판 글 목록 출력 ], [ 더미데이터 추가 ]
7차 일지 - [ 페이징 ], [ 게시글 디테일 출력 ]

8차 일지 - [ 게시글 작성 ], [ 게시글 삭제 ], [ 조회수 증가 ]

 

1. 게시글 작성

  • boardList.jsp
더보기
<script type="text/javascript">
	$(()=>{
		let m = '${msgFl}';
		if (m!=""){
			alert(m);
		}
		let msg = '${msg}'
		if(msg!=""){
			Swal.fire({
				icon : "success",
				title : "게시글 작성이 완료되었습니다.",
				timer: 3000,
				timerProgressBar: true,
				//showConfirmButton: false,
			});
			msg=""
		}
	})
</script>
  • 게시글 작성 완료를 리스트에서 한 이유는 완료 후 커스텀 얼럿창을 띄우니 띄워짐과 동시에 넘어가고 지연함수를 사용해도 세션 당 한 번만 제대로 지연이 되어버려서 바깥으로 뺄 수 밖에 없었다..( 꾸밈은 내쪽이 아니니까 라는 변명으로 넘어가기로 했다. )

 

<c:if test="${not empty loginId}">
    <button type="button" class="crudBtn" onclick="location.href='/board/boardWrite'"
			style="float: left;">글 작성</button>
</c:if>
  • 게시글 목록 윗쪽에 글 작성을 할 수 있도록 만들었다.
  • 회원만 작성할 수 있도록 했으므로 c:if를 통해 로그인 시에만 해당 버튼이 보이도록 했다. 

 

  • boardWrite.jsp
더보기
<script type="text/javascript">
	$(()=>{
		let id = '${loginId}'
		document.getElementById('bWriter').value=id;
	})
</script>
  • 페이지가 로딩되면 실행하는 스크립트
  • 세션에 저장되어있는 로그인 아이디를 밑의 hidden타입의 밸류에 입력해준다.

 

<jsp:include page="./header.jsp"></jsp:include>

<div class="allBigBox">
    <h3>Board Write</h3>

    <div id="bDetailBox">

        <form id="write_frm" method="get" onsubmit="return writeCheck()"
            action="/board/writeSub">
            <div id="bWrTitleDiv">
                <input type="text" id="bTitle" name="bTitle"
                    placeholder="제목은 최대 15글자까지 적을 수 있습니다.">
            </div>
            <br>
            <input type="hidden" id="bWriter" name="bWriter">

            <textarea id="summernote" name="bContents"></textarea>
            <br>

            <div class="btnDiv">
                <button type="button" onclick="location.href = document.referrer;">취소</button>
                <button type="submit" class="crudBtn">올리기</button>
            </div>
        </form>

    </div>
</div>
  • 페이지가 로딩되면 실행되는 스크립트가 있기 때문에 hidden의 밸류는 굳이 정해주지 않았다. 

 

<script>
    function writeCheck() {
        let ok = false
        let title = $('#bTitle').val()
        var contents = $('#summernote').summernote('code')

        if (title == '') {
            Swal.fire({
                icon : "error",
                title : "Oops...",
                text : "제목은 비워둘 수 없습니다.",
            });
            return false
        } else if (title.length > 15) {
            Swal.fire({
                icon : "error",
                title : "Oops...",
                text : "제목은 15글자까지 작성할 수 있습니다.",
            });
            return false
        } else if (contents == '<p><br></p>') {
            Swal.fire({
                icon : "error",
                title : "Oops...",
                text : "내용은 비워둘 수 없습니다.",
            });
            return false
        }
        return true
    }

    $(document).ready(function() {
        $('#summernote').summernote({
            lang : 'ko-KR' // default: 'en-US'
        });
    });

    $('#summernote').summernote({
                placeholder : '게시글을 작성해주세요.',
                tabsize : 2,
                height : 400,
                toolbar : [ [ 'style', [ 'style' ] ],
                        [ 'font', [ 'bold', 'underline', 'clear' ] ],
                        [ 'color', [ 'color' ] ],
                        [ 'para', [ 'ul', 'ol', 'paragraph' ] ],
                        [ 'table', [ 'table' ] ],
                        [ 'insert', [ 'link', 'picture', 'video' ] ],
                        [ 'view', [ 'fullscreen', 'codeview', 'help' ] ] ]
            });
</script>
  • 제목이 비어있거나 15글자를 초과한 경우, 게시글 내용이 비어있는 경우를 걸러주었다.
  • 밑의 두 스크립트는 썸머노트와 관련된 스크립트이다.

 

  • BoardController.java
더보기
@GetMapping("/board/writeSub")
public String writeSub(BoardDto bDto, RedirectAttributes ra) {
    log.info("==> GetMapping - writeSub 요청 <==");
    log.info("=======bDto{}",bDto);

    if(bSer.writeSub(bDto)) {
        ra.addFlashAttribute("msg","ok");
    }

    return "redirect:/board/list?page=1";
}
  • 글 작성 완료 후에는 리스트 페이지 1로 돌아가도록 했다.

 

  • BoardService.java
더보기
public boolean writeSub(BoardDto bDto) {
    System.out.println("==== bSer -> writeSub");
    return bDao.writeSub(bDto);
}

 

  • BoardDao (java, xml)
더보기
public boolean writeSub(BoardDto bDto);

 

<insert id="writeSub" parameterType="BoardDto">
    insert into board values(default,#{bTitle},#{bContents},#{bWriter},default,default)
</insert>
  • 값을 #{변수명}으로 해야하는데 계속 ${변수명}으로 하면서 어디서 에러가 나는지 전혀 몰랐다.
  • 바로 윗쪽에서 $를 사용했어서 똑같이 사용했던 것인데, 윗쪽에선 제대로 됐던 것이 어이가 없을 뿐이었다..이걸 고치면서 위 구문도 바꿔주었다.

 

  • 결과
더보기
  • 글 작성 버튼
로그인 안 한 상태에선 글 작성이 안 보임(생성이 안 됨)
로그인 시 글 작성이 보임

 

  •  작성 화면
썸머 노트를 사용해서 유저가 알아서 글을 꾸밀 수 있도록 했다.

 

  • 오류
전부 비우고 올리기 버튼 클릭 시
글 작성 후 제목만 비우고 올리기 버튼 클릭 시
제목의 길이가 15글자를 초과한 상태에서 올리기 버튼 클릭 시
제목 작성 후 글만 비우고 올리기 버튼 클릭 시

 

  • 게시글 작성에 성공했을 경우
OK 버튼을 클릭하거나 시간이 지나면 얼럿창이 사라진다.

 

 

  • 썸머노트로 작성한 이유
작성 시 test내용으로만 적었는데 대충 설정 사용하여 작성하면 <p>test 내용</p><p><b>test 내용</b></p> 이런 식으로 저장되어서 일반 블로그를 작성할 때처럼&nbsp; 간편하게 올릴 수 있다.

 

 

2. 게시글 삭제

  • boardDetail.jsp
더보기
<button type="button" class="crudBtn"
		onclick="location.href='/board/boardDelete?bNum=${bDto.bNum}'">삭제</button>
  • button에 onclick을 통해 링크를 달아준다.
  • 그냥 /board/boardDelete?bNum=${bDto.bNum} 이것만 작성했다가 검색해보니 버튼에 링크를 넣을거면 location.href를 넣어줘야한다는 것을 알았다. (이전에 사용했었는데 까먹었던 걸지두)

 

  • BoardController.java
더보기
@GetMapping("/board/boardDelete")
public String bDelete(BoardDto bDto, RedirectAttributes ra) {
    log.info("==> GetMapping - bDelete 요청 <==");
    log.info("=======bDto{}",bDto);

    if(bSer.bDelete(bDto.getbNum())) {
        ra.addFlashAttribute("msg","bDelete");
    }

    return "redirect:/board/list?page=1";
}

 

  • BoardService.java
더보기
public boolean bDelete(int bNum) {
    System.out.println("==== bSer -> bDelete");
    return bDao.bDelete(bNum);
}

 

  • BoardDao (java, xml)
더보기
public boolean bDelete(int bNum);

 

<delete id="bDelete">
    delete from board where bNum=#{bNum}
</delete>

 

  • 결과
더보기
  • 게시글
본인 게시글이 아닐 때
본인 게시글일 때

 

  •  삭제 완료 알림

 

 

3. 조회수 증가

  • 조회수 증가는 다중 쿼리문을 사용해야 했는데 멀티쿼리 true를 사용하지 않는 방법을 찾아보려다 한계를 느끼고 결국 멀티쿼리 설정을 바꾸게 되었다.
  • application.properties
더보기
spring.datasource.url=jdbc:log4jdbc:mysql://localhost:3306/mypro?allowMultiQueries=true
  • 해당 파일(resources 폴더에 들어있다.)에 들어가서 sql 링크 뒤에 ?allowMultiQueries=true를 붙이면 설정이 바뀌게 되어 멀티쿼리를 작성할 수 있게 된다.
  • 혹시 몰라 밑에 파일의 전문을 붙여넣었다.

 

server.port=80

# jsp,thymeleaf static resource 폴더 지정(기본값,생략)
spring.web.resources.static-locations=classpath:static/

# JSP뷰 설정- main/webapp/WEB-INF/views폴더 생성
spring.mvc.view.prefix=/WEB-INF/views/
spring.mvc.view.suffix=.jsp

# DB설정
spring.datasource.driver-class-name=net.sf.log4jdbc.sql.jdbcapi.DriverSpy
spring.datasource.url=jdbc:log4jdbc:mysql://localhost:3306/mypro?allowMultiQueries=true
spring.datasource.username=persP
spring.datasource.password=1234

# Mybatis 설정
mybatis.mapper-locations=classpath:mappers/**/*.xml
mybatis.type-aliases-package=com.myPro.persP.dto

 

  • BoardDao.xml
더보기
<select id="bDetail">
    update board set bWatch=bWatch+1 where bNum=#{bNum};
    select * from board where bNum=#{bNum};
</select>
  • select 후 update를 사용하면 update된 것으로 보이지 않아서 순서가 중요하다!

 

  • 결과
더보기
  • 게시판 리스트
맨 윗 줄의 조회수는 2

 

  • 조회수 증가
게시글에 들어와서 보이는 조회수는 3

 

  • 리스트
리스트로 돌아와서 보이는 217번 [test 제목]의 조회수는 3

 


 

전체 피드백

 


728x90

'프로그래밍 > +a' 카테고리의 다른 글

slPro 7차 일지  (0) 2024.01.15
slPro 6차 일지  (1) 2024.01.11
slPro 5차 일지  (0) 2024.01.09
slPro 4차 일지  (1) 2024.01.08
미니 팀 프로젝트 중간 과정 (파이썬)  (1) 2024.01.03
728x90

 페이징은 팀장(현재는 전팀장인..!) 도움으로 해놨으나 파이널 프로젝트에 들어가기 전에 푹 쉬고 오느라 간단한 게시글 출력과 일지 작성이 늦어졌다.

1차 일지 - [ 설정 ], [ 회원가입 ]
2차 일지 - [ 로그인 ]
3차 일지 - [ index에서 로그인 상태에 따라 display none/block 제어하고 p태그 내용 바꾸기 ], [ 로그아웃 ]
4차 일지 - [ header에 회원 아이디, 회원정보/로그아웃 링크 or 비회원 문구 추가 후 로그인 상태에 따라 display none/block 제어 ], [ 일반 회원 정보 확인 ]
5차 일지 - [ 일반 회원 정보 출력 변경 ], [ 관리자) 회원 정보 목록 출력 ], [ 일반 회원 비밀번호 변경 ]
6차 일지 - [ 게시판 글 목록 출력 ], [ 더미데이터 추가 ]

7차 일지 - [ 페이징 ], [ 게시글 디테일 출력 ]

 

1. 페이징

더보기
private int nowP;	// 현재 페이지
private int firstP = 1;	// 맨 첫번째 페이지
private int lastP;	// 맨 마지막 페이지
private int leftP;	// 현재 페이지의 첫번째 페이지
private int rightP;	// 현재 페이지의 마지막 페이지
private int beforeP;	// leftP -p
private int nextP;	// leftP +p
private int pageSize = 10;

public Paging(int nowP, int bCnt) {
    this.nowP = nowP;
    lastP = (int)Math.ceil(bCnt/pageSize)+1;
    leftP = ((int)(Math.floor((nowP-1)/pageSize))*pageSize) +1;
    rightP = leftP +pageSize -1;
    beforeP = leftP -pageSize;
    nextP = leftP +pageSize;
}
  • 이걸 직접 계산하느라 너무 힘들었다..

 

  • BoardController.java
더보기
@GetMapping("/board/list")
public String boardList(@RequestParam(name="page")int page, Model model, RedirectAttributes ra) {
    log.info("==> GetMapping - boardList 요청 <==");

    int limit = (page-1)*10;
    ArrayList<BoardDto> bList = bSer.bList(limit);
    model.addAttribute("bList", bList);
    System.out.println("==> bList: {}"+bList);

    int bCnt = bSer.getBCnt();

    Paging paging = new Paging(page, bCnt);
    model.addAttribute("paging",paging);
    System.out.println("==> paging: {}"+paging);

    return "boardList";
}
  • 페이징을 추가하느라 업데이트했다.
  • 처음엔 생각없이 bList의 사이즈를 모든 게시글의 갯수라고 적었었는데 오류를 발견하고 다시 갯수를 세어오는 메소드를 생성했다.
  • 해당 메소드로 얻어온 모든 게시글의 갯수와 현재 페이지를 매개변수로 페이징의 생성자를 통해 다른 값을 생성한다.

 

  • BoardService.java
더보기
public int getBCnt() {
    System.out.println("==== bSer -> getBCnt");
    return bDao.getBCnt();
}
public BoardDto bDetail(int bNum) {
    System.out.println("==== bSer -> bDtail");
    return bDao.bDetail(bNum);
}

 

  • BoardDao (java, xml)
더보기
public int getBCnt();
public BoardDto bDetail(int bNum);

 

<select id="getBCnt">
    select count(*) from board
</select>
<select id="bDetail">
    select * from board where bNum=${bNum}
</select>

 

  • boardList.jsp
더보기
<table class="pagingT" style="width: 500px; max-width: 800px; text-align: center;">
    <tr>
        <c:if test="${paging.leftP!=1}">
            <td><a href="/board/list?page=1"><<</a></td>
            <td><a href="/board/list?page=${paging.beforeP}"><</a></td>
        </c:if>

            <c:forEach var="i" begin="${paging.leftP}" end="${paging.rightP}">
                <c:if test="${i <= paging.lastP}">
                    <td><a href="/board/list?page=${i}">${i}</a></td>
                </c:if>
            </c:forEach>

        <c:if test="${paging.rightP<paging.lastP}">
            <td><a href="/board/list?page=${paging.nextP}">></a></td>
            <td><a href="/board/list?page=${paging.lastP}">>></a></td>
        </c:if>
    </tr>
</table>
  • 페이징을 하면서 c:forEach로 일반 for문 돌리는 것도 처음 사용해봤다.
  • 기존에 for문에서 i=0; i<=10; i++을 사용했던 것처럼 var을 i로 사용하고, begin에 시작할 i의 수를 적고, begin을 i가 언제까지 돌 것인가(끝)를/을 적어 사용한다.

 

  • 결과
더보기
게시판 첫 화면
5페이지 클릭 시
다음장(>) 클릭 시
끝(>>) 클릭 시

 

 

2. 게시글 디테일 출력

  • boardList.jsp
더보기
<table class="bListTable">
    <tr>
        <th>&ensp;글 번호&ensp;</th>
        <th>&ensp;제목&ensp;</th>
        <th>&ensp;작성자&ensp;</th>
        <th>&ensp;작성 날짜&ensp;</th>
        <th>&ensp;조회수&ensp;</th>
    </tr>
    <c:if test="${empty bList}">
        <tr>
            <td>글이 존재하지 않습니다.</td>
        </tr>
    </c:if>
    <c:if test="${!empty bList}">
        <c:forEach items="${bList}" var="b">
            <tr>
                <td>&ensp;${b.bNum}&ensp;</td>
                <td><a href="/board/boardDetail?bNum=${b.bNum}">&ensp;${b.bTitle}&ensp;</a></td>
                <td>&ensp;${b.bWriter}&ensp;</td>
                <td>&ensp;${b.bWDate}&ensp;</td>
                <td>&ensp;${b.bWatch}&ensp;</td>
            </tr>
        </c:forEach>
    </c:if>
</table>
  • 글 제목에 a태그를 통해 디테일로 넘어갈 수 있도록 했다.

 

  • BoardController.java
더보기
@GetMapping("/board/boardDetail")
public String boardDetail(BoardDto bDto, Model model) {
    bDto = bSer.bDetail(bDto.getbNum());

    model.addAttribute("bDto", bDto);
    return "boardDetail";
}

 

  • BoardService.java
더보기
public BoardDto bDetail(int bNum) {
    System.out.println("==== bSer -> bDtail");
    return bDao.bDetail(bNum);
}

 

  • BoardDao (java, xml)
더보기
public BoardDto bDetail(int bNum);

 

<select id="bDetail">
    select * from board where bNum=${bNum}
</select>

 

  • boardDetail.jsp
더보기
<h3>Board Detail</h3>

<div class="allBigBox">

    <div id="bDetailBox">
        <p>글번호: ${bDto.bNum}</p>
        <p>제목: ${bDto.bTitle}</p>
        <p>내용: ${bDto.bContents}</p>
        <p>작성자: ${bDto.bWriter}</p>
        <p>작성날짜: ${bDto.bWDate}</p>
        <p>조회수: ${bDto.bWatch}</p>
        <c:if test="${loginId==bDto.bWriter}">
            <button type="button" class="crudBtn" onclick="history.back()">수정(만들예정)</button>
            <button type="button" class="crudBtn" onclick="history.back()">삭제(만들예정)</button>
        </c:if>
    </div>

    <button type="button" onclick="history.back()">리스트로 돌아가기</button>
</div>

 

  • 결과

 


728x90

'프로그래밍 > +a' 카테고리의 다른 글

slPro 8차 일지  (0) 2024.01.18
slPro 6차 일지  (1) 2024.01.11
slPro 5차 일지  (0) 2024.01.09
slPro 4차 일지  (1) 2024.01.08
미니 팀 프로젝트 중간 과정 (파이썬)  (1) 2024.01.03
728x90

6차에서 헤더랑 좀 변경하긴 했는데 css쪽이라 서술하진 않으려고 한다.
팀원분이 페이징하는 것을 구경하다가 결국 출력 및 테스트 더미데이터 넣기만 하고 끝났다.
미리 공부한 셈 치고 7차에서는 디테일, 페이징을 넣어볼 생각이다.

1차 일지 - [ 설정 ], [ 회원가입 ]
2차 일지 - [ 로그인 ]
3차 일지 - [ index에서 로그인 상태에 따라 display none/block 제어하고 p태그 내용 바꾸기 ], [ 로그아웃 ]
4차 일지 - [ header에 회원 아이디, 회원정보/로그아웃 링크 or 비회원 문구 추가 후 로그인 상태에 따라 display none/block 제어 ], [ 일반 회원 정보 확인 ]
5차 일지 - [ 일반 회원 정보 출력 변경 ], [ 관리자) 회원 정보 목록 출력 ], [ 일반 회원 비밀번호 변경 ]

6차 일지 - [ 게시판 글 목록 출력 ], [ 테스트용 글 더미데이터 100개 입력 ]

 

1. 게시판 글 목록 출력

  • header.jsp
더보기
<div class="headerBigBox" style="min-height: 180px; background-color: #2e2e34;">
	<nav class="top-right" style="">
    
		<div class="headerBox">
			<nav id="loginIdHeaderOn" style="padding: 15px 10px 0px 0px; float: right; display: none;">
				<p id="loginIdHeader" style="color: #fff; float: left;">test님</p>
				&ensp;<a href="/member/info" id="checkAM">내 정보 확인</a>
				&ensp;<a href="/member/logout">로그아웃</a>&ensp;
			</nav>
			<nav id="loginIdHeaderOff" style="padding: 15px 10px 0px 0px; float: right; display: block;">
				<p id="loginIdHeader" style="color: #fff; float: left;">현재 로그인 상태가 아닙니다.</p>
			</nav>
		</div>
		
		<ul class="top-menu" style="color: #d8d3cd">
			<li><a>menu1</a></li>
			<li><a href="/board/list?page=1">게시판</a></li>
			<li><a>menu3</a></li>
			<li><a>menu4</a></li>
		</ul>
        
	</nav>
</div>
  • 메뉴 2번을 게시판으로 변경했다.
  • 페이징을 넣을 것이기 때문에 링크를 넘어갈 때 페이지를 지정해서 넘겨줬다.

 

  •  BoardController.java
더보기
@Controller
@Slf4j
public class BoardController {
	@Autowired
	private BoardService bSer;
	
	@GetMapping("/board/list")
	public String boardList(@RequestParam(name="page")int page, Model model, RedirectAttributes ra) {
		log.info("==> GetMapping - boardList 요청 <==");
		
		ArrayList<BoardDto> bList = bSer.bList(page);
		model.addAttribute("bList", bList);
		log.info("==> bList: {}",bList);
		
		return "boardList";
	}
}
  • 넘겨준 페이지는 @RequestParam으로 받아주고, 글을 받아오기 위해 BoardDto가 담긴 ArrayList를 생성했다.
  • model에 받아온 ArrayList를 저장하고 리턴한다.

 

  • BoardService.java
더보기
public ArrayList<BoardDto> bList(int page) {
    System.out.println("==== bSer -> bList");
    return bDao.bList(page);
}

 

  • BoardDao (java, xml)
더보기
public ArrayList<BoardDto> bList(int page);
<select id="bList">
    select bNum, bTitle, bWriter, bWDate, bWatch from board order by bNum limit 0,10
</select>
  • 나중에 페이징을 넣을때 여기에서 limit에 적은 0에 받아온 값을 넣을 예정이다.

 

  • boardList.jsp
더보기
<jsp:include page="./header.jsp"></jsp:include>

<div class="allBigBox">
    <table class="bListTable">
        <tr>
            <th>&ensp;글 번호&ensp;</th>
            <th>&ensp;제목&ensp;</th>
            <th>&ensp;작성자&ensp;</th>
            <th>&ensp;작성 날짜&ensp;</th>
            <th>&ensp;조회수&ensp;</th>
        </tr>
        <c:if test="${empty bList}">
            <tr>
                <td>글이 존재하지 않습니다.</td>
            </tr>
        </c:if>
        <c:if test="${!empty bList}">
            <c:forEach items="${bList}" var="b">
                <tr>
                    <td>&ensp;${b.bNum}&ensp;</td>
                    <td>&ensp;${b.bTitle}&ensp;</td>
                    <td>&ensp;${b.bWriter}&ensp;</td>
                    <td>&ensp;${b.bWDate}&ensp;</td>
                    <td>&ensp;${b.bWatch}&ensp;</td>
                </tr>
            </c:forEach>
        </c:if>
    </table>

    <br>
    <button type="button" onclick="location.href='/'">메인화면</button>
</div>
  • body
  • 변수명을 두번째 글자에 대문자를 자주 썼는데 이번에 c:forEach를 사용하려니까 문제가 생겼었다. 이 문제를 해결하려고 많은 애를 썼는데, 아직 검색하는 법을 잘 몰라서 서칭도 오래 걸렸다.

 

  • BoardDto.java
더보기
//@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
private int bNum;	// 글번호
private String bTitle;	// 글 제목
private String bContents;	// 글 내용
private String bWriter;	// 작성자
private String bWDate;	// 작성시간/날짜
private String bWatch;	// 조회수
public int getbNum() {
    return bNum;
}
public String getbTitle() {
    return bTitle;
}
public String getbContents() {
    return bContents;
}
public String getbWriter() {
    return bWriter;
}
public String getbWDate() {
    return bWDate;
}
public String getbWatch() {
    return bWatch;
}
public void setbNum(int bNum) {
    this.bNum = bNum;
}
public void setbTitle(String bTitle) {
    this.bTitle = bTitle;
}
public void setbContents(String bContents) {
    this.bContents = bContents;
}
public void setbWriter(String bWriter) {
    this.bWriter = bWriter;
}
public void setbWDate(String bWDate) {
    this.bWDate = bWDate;
}
public void setbWatch(String bWatch) {
    this.bWatch = bWatch;
}
  • 위에서 말한 에러코드는 이랬다. -> c:forEach Property [bnum] not found on type [cohttp://m.myPro.persP.dto.BoardDto]
  • @Data를 이용해서 간편하게 사용하려고 했는데 내가 지정한 변수명이 두번째 글자들이 대문자이다 보니 lombok의 Getter, Setter가 getBNum, getBTitle 이런 식으로 생성해서 컴퓨터가 이해할 수 없다? 고 했던 것 같다. 그래서 수동으로 sts에서 Getter, Setter를 만들어주는 것을 통해 추가해주었다.

 

  • 결과
더보기
  • 아직 디테일은 만들지 않았다.

 

 

 

2. 더미 데이터 추가

더보기
  • 프로젝트를 우클릭하면 나오는 창에서 맨 밑의 Properties를 클릭한다.
  • 왼쪽 박스에서 Java Build Path를 들어간 뒤, Libraries를 클릭한다.
  • Classpath를 클릭 후, Add Library를 해준다.  (위의 Modulepath에 하면 내가 한 방식으로는 에러가 난다!)
  • JUnit을 누른 후, Next>Finish를 차례로 눌러준다.  (Next 시 버전을 선택할 수 있다.)
  • Apply and Close를 통해 추가해준다.
  • 세번째 폴더의 src/test/java를 들어간다.
  •  더미데이터를 추가할 패키지/파일을 만든다.
  •  클래스에 @SpringBootTest를 추가하고, 더미데이터로 추가할 내용을 작성한다.
@SpringBootTest
class BoardDtoTest {

	@Autowired
	private BoardDao bDao;
	@Test
	//@Transactional
	void insertDummyDataTest() {
		BoardDto bDto=new BoardDto();
		for(int i=0;i<100;i++) {
			bDto.setbTitle("제목"+i);
			bDto.setbContents("내용"+i);
			bDto.setbWriter("aaa");
			bDao.insertDummyData(bDto);
		}
		
	}

}
  • dao의 xml이나 java에서 insert 구문을 적는다. (나는 java 파일에 작성했다.)
// Test용 더미 데이터
@Insert("insert into board values(default,#{bTitle},#{bContents},#{bWriter},default,default)")
public void insertDummyData(BoardDto bDto);
  • 다시 더미데이터로 추가할 내용을 작성해준 클래스로 돌아가서 우클릭을 통해 Run As > 2JUnit Test를 클릭한다.
  • 왼쪽 네모박스에서 결과를 확인할 수 있다!

 


 

전체 피드백

  • order by 거꾸로 뒤집어야했는데 까먹었다..잊지말고 바꿀것!!

 


 

728x90

'프로그래밍 > +a' 카테고리의 다른 글

slPro 8차 일지  (0) 2024.01.18
slPro 7차 일지  (0) 2024.01.15
slPro 5차 일지  (0) 2024.01.09
slPro 4차 일지  (1) 2024.01.08
미니 팀 프로젝트 중간 과정 (파이썬)  (1) 2024.01.03
728x90

 멤버 다음은 lol api 사용해서 정보 가져와서 뿌리는거 하려고 했는데 같은 조원 분이 게시판이 중요한 것이라고 하셔서 다음 단계는 게시판으로 결정했다.


1차 일지 - [ 설정 ], [ 회원가입 ]
2차 일지 - [ 로그인 ]
3차 일지 - [ index에서 로그인 상태에 따라 display none/block 제어하고 p태그 내용 바꾸기 ], [ 로그아웃 ]
4차 일지 - [ header에 회원 아이디, 회원정보/로그아웃 링크 or 비회원 문구 추가 후 로그인 상태에 따라 display none/block 제어 ], [ 일반 회원 정보 확인 ]

5차 일지 - [ 일반 회원 정보 출력 변경하면서 관리자 로그인 시 회원 정보 출력 추가 ], [ 일반 회원 비밀번호 변경 ]

 

1. 회원 정보 출력

 일반 회원 정보 출력을 관리자랑 같이 사용하려면 변경이 필요할 것 같아서 조금 변경하다보니 거의 다 변경하게 되었다..
 MemberController는 변경점이 없어서 빼고 작성했다.
  • memberInfo.jsp
더보기
  • body
<p>현재 로그인 중인 id: ${loginId}</p>

<br>
<div id="info">
</div>

 

  • script
$(()=>{
	let loginId = '<%=(String)session.getAttribute("loginId")%>';
	
	// ============================ 회원 정보 관련 ================================
	$.ajax({
        method: 'get',
        url: 'memberInfo',
    }).done(function(res){
        console.log("res: ",res);
        const mList = res;

        if (loginId == "admin"){
            $("#info").html(" => 회원 목록");
            for (i=0; i<mList.length; i++){
                $("#info").append("<p>=================================</p>");
                $("#info").append("<p>이름: "+mList[i].mname+"</p>");
                $("#info").append("<p>이메일: "+mList[i].memail+"</p>");
                if (!(mList[i].gname <= 0)){
                    $("#info").append("<p>게임 닉네임: "+mList[i].gname+"</p>");
                }
                $("#info").append("<p>가입 날짜: "+mList[i].joinDate+"</p>");
                if (i==mList.length-1){
                    $("#info").append("<p>=================================</p>");
                }
            }

        }else{
            $("#info").html("<p>이름: "+mList[0].mname+"</p>");
            $("#info").append("<p>이메일: "+mList[0].memail+"</p>");
            if (!(mList[0].gname <= 0)){
                $("#info").append("<p>게임 닉네임: "+mList[0].gname+"</p>");
            }
            $("#info").append("<p>가입 날짜: "+mList[0].joinDate+"</p>");

            document.getElementById("infoPwCh").style.display = 'block';
        }

    }).fail((err,status)=>{
        console.log("err:", err);
        console.log("status:", status);
        $("#info").html("에러!").css('color','red');

    })
})
  • $(()=>{}): 페이지가 로딩되면 바로 실행하는 함수
  • 4일차에서는 모든 정보를 비밀번호를 받고나서 띄우게 했었는데, 비밀번호를 변경할 때만 비밀번호를 받도록 변경했다.
  • 세션에 저장된 아이디가 admin인 경우, 아닌 경우를 나누어 회원 정보 또는 회원 정보 리스트를 띄웠다. (조건으로 리스트의 길이로 줘도 괜찮았을 것 같다!)
  • gName인 게임 닉네임이 가장 큰 고난이었는데, 조건으로 gname >= 0, gname <= 0 둘 다 해봤는데(후자는 안 될 것을 알고 해봤다.) 널 값만 잘 띄우길래 포기하고 하나로 묶어 앞에 !(느낌표)를 적어 조건을 뒤집어줬다. 

 

  • MemberAsyController.java
더보기
@GetMapping("/member/memberInfo")
public ArrayList<MemberDto> memberInfo(HttpSession session) {
    log.info("==> GetMapping - memberInfo 요청 <==");
    String mId = session.getAttribute("loginId").toString();
    return mSer.memberInfo(mId);
}
  • 4일차에서 return 타입을 MemberDto로 했었는데, ArrayList<MemberDto>로 변경했다.

 

  • MemberService.java
더보기
public ArrayList<MemberDto> memberInfo(String mId) {
    log.info("===> mSer memberInfo 요청 <===");
    ArrayList<MemberDto> mList = new ArrayList<>();
    if (mId.equals("admin")) {
        mList = mDao.mListInfo(mId);
    }else {
        MemberDto mDto = mDao.memberInfo(mId);
        mList.add(mDto);
    }
    return mList;
}
  • MemberAsyController에서 return 타입을 MemberDto에서 ArrayList<MemberDto>로 변경함에 따라 해당 메소드도 return 타입을 동일하게 변경했다.
  • 매개변수로 받아온 id가 admin인지 아닌지에 따라 MemberDao의 메소드를 다르게 사용하여 mList에 담아서 그걸 return했다.

 

  • MemberDao (java, xml)
더보기
  • 일반 회원 메소드
MemberDto memberInfo(String mId);	// .java
<select id="memberInfo" parameterType="String">
    select mId, mName, mEmail, gName, joinDate from member where mId=#{param1}
</select>				// .xml

 

  • admin(관리자) 메소드
ArrayList<MemberDto> mListInfo(String admin);	// .java
<select id="mListInfo">
    select mId, mName, mEmail, gName, joinDate from member where mId!=#{param1}
</select>				// .xml

 

  • 결과
더보기
  • 일반 회원
회원 정보 출력 / 비밀번호 변경은 2번에서 후술.

 

  • admin(관리자)
회원 정보 출력

 

 

2. 일반 회원 비밀번호 변경

  • memberInfo.jsp
더보기
  • body
<div id="infoPwCh" style="display: none;">
    <p>비밀번호를 변경하시려면 현재 비밀번호를 입력하세요.</p>
    <input type="password" name="mPw" id="infoMPw" class="input" placeholder="비밀번호를 입력하세요" maxlength="20" autocapitalize="off">
    <div id="infoCh" style="display: none;"></div>
    <button type="button" class="btn btn-3 btn-3e" id="infoSubBtn" style="text-align: center;">비밀번호 확인</button>
</div>

<div id="changePw" style="display: none;">
    <input type="password" name="changeMPw" id="changeMPw" class="input" placeholder="변경할 비밀번호를 입력하세요" maxlength="20" autocapitalize="off">
    <input type="password" name="changeMPwRe" id="changeMPwRe" class="input" placeholder="변경할 비밀번호 재입력" maxlength="20" autocapitalize="off">
    <button type="button" class="btn btn-3 btn-3e" id="changeMPwBtn" style="text-align: center;">비밀번호 변경</button>
    <p id="changeAlert" style="display: none;"></p>
</div>

 

  • script
// ============================ 비밀번호 변경 관련1 ================================
$('#infoSubBtn').on('click',function(){			// 현재 비밀번호 확인
    let pw = $('#infoMPw').val();
    if (pw==''){
        $('#infoCh').html('비밀번호를 입력해주세요.').css('color','red');
        document.getElementById("infoCh").style.display = 'block';
        $('#infoMPw').focus();
        return;
    }
    document.getElementById("infoCh").style.display = 'none';
    let chPwSend = {mId:loginId, mPw:pw};
    $.ajax({
        methoed: "get",
        url: "pwCheck",
        data: chPwSend,
    }).done(function(res){
        if (!(res <= 0)){
            document.getElementById("changePw").style.display = 'block';
            document.getElementById("infoPwCh").style.display = 'none';
        }else{
            $('#infoCh').html('비밀번호가 틀렸습니다.').css('color','red');
            document.getElementById("infoCh").style.display = 'block';
        }
    }).fail((err,status)=>{
        console.log("err:", err);
        console.log("status:", status);
        document.getElementById("infoCh").style.display = 'block';
        $("#infoCh").html("비밀번호가 틀렸습니다.").css('color','red');

    })
})
// ============================ 비밀번호 변경 관련2 ================================
let usePw = false;

$('#changeMPwRe').on('click',function(){	// 비밀번호 체크
    let pw = $('#changeMPw').val();
    let chPw = $('#changeMPwRe').val();
    let con = document.getElementById("changeAlert");
    con.style.display = 'block';
    if (pw==''){
        $('#changeAlert').html("변경할 비밀번호를 먼저 입력해주세요.").css("color",'red');
        $('#changeMPw').focus();
        return;
    }
})
$('#changeMPwRe').blur(function(){
    let pw = $('#changeMPw').val();
    let chPw = $('#changeMPwRe').val();
    let con = document.getElementById("changeAlert");
    con.style.display = 'block';
    if (pw!='' && chPw==''){
        $('#changeAlert').html("비밀번호 확인 칸은 비워둘 수 없습니다.").css("color",'red');
        $('#changeMPwRe').focus();
        return;
    }
})
$('#changePw').on('keyup', function(){
    let pw = $('#changeMPw').val();
    let chPw = $('#changeMPwRe').val();
    let con = document.getElementById("changeAlert");
    con.style.display = 'block';
    if (pw!=''){
        if(pw.length<=20 && pw.length>=8){
            $('#changeAlert').html("");
        }else if(pw.length>20 || pw.length<8){
            $('#changeAlert').html("비밀번호의 길이는 8~20자 사이로 정해주세요.").css("color",'red');
            $('#changeMPw').focus();
            return;
        }
        if(chPw!='' && pw!=chPw){
            $('#changeAlert').html("비밀번호가 일치하지 않습니다.").css("color",'red');
            $('#changeMPwRe').focus();
        }else if(pw==chPw){
            usePw = true;
            con.style.display = 'none';
        }
            return;
    }
    usePw = false;
});			// 비밀번호 체크 끝


$('#changeMPwBtn').on('click',function(){	// 비밀번호 변경
    let changePw = $('#changeMPw').val();
    let changePwRe = $('#changeMPwRe').val();

    if (!usePw){
        document.getElementById("changeAlert").style.display = 'block';
        $('#changeAlert').html("비밀번호를 확인해주세요.").css("color",'red');
        return;
    }

    let changePwSend = {mId:loginId, changeMPw:changePw} 
    $.ajax({
        methoed: "get",
        url: "changeMPw",
        data: changePwSend,
    }).done(function(res){
        if (res == "ok"){
            alert("비밀번호를 성공적으로 변경했습니다.");
            document.getElementById('infoMPw').value = null;
            document.getElementById('changeMPw').value = null;
            document.getElementById('changeMPwRe').value = null;
            /* $('#infoMPw').value = null;
            $('#changeMPw').value = null;
            $('#changeMPwRe').value = null; */
            document.getElementById("changePw").style.display = 'none';
            document.getElementById("changeAlert").style.display = 'none';
            document.getElementById("infoPwCh").style.display = 'block';
        }else{
            $('#changeAlert').html('비밀번호를 변경하지 못했습니다.').css('color','red');
            document.getElementById("changeAlert").style.display = 'block';
        }
    }).fail((err,status)=>{
        console.log("err:", err);
        console.log("status:", status);
        document.getElementById("changeAlert").style.display = 'block';
        $("#changeAlert").html("비밀번호를 변경하지 못했습니다.").css('color','red');

    })

})

 새로운 비밀번호를 입력하고 또 그 비밀번호를 확인하는 것 때문에 스크립트가 길어졌지만 회원가입 할 때와 스크립트가 유사하기 때문에 마지막 스크립트만 시간이 걸렸다.

 

  • MemberAsyController.java
더보기
  • 현재 비밀번호 확인
@GetMapping("/member/pwCheck")
public String pwCheck(@RequestParam(name = "mId")String mId, @RequestParam(name = "mPw")String mPw) {
    log.info("==> GetMapping - pwCheck 요청 <==");
    MemberDto mDto = new MemberDto();
    mDto.setMId(mId);
    mDto.setMPw(mPw);
    if(mSer.login(mDto)) {
        return "ok";
    }
    return null;
}

 간단하게 id만 가져와서 로그인 할 때 사용했던 메소드를 사용하여 비밀번호가 맞는지 확인했다.

 

  • 새로운 비밀번호로 변경
@GetMapping("/member/changeMPw")
public String changeMPw(@RequestParam(name = "mId")String mId, @RequestParam(name="changeMPw")String changeMPw) {
    log.info("==> GetMapping - changeMPw 요청 <==");
    MemberDto mDto = new MemberDto();
    mDto.setMId(mId);
    mDto.setMPw(changeMPw);
    if(mSer.changeMPw(mDto)) {
        return "ok";
    }
    return "no";
}

 

  • MemberService.java
더보기
public boolean changeMPw(MemberDto mDto) {
    log.info("===> mSer changeMPw 요청 <===");
    BCryptPasswordEncoder pwEn = new BCryptPasswordEncoder();
    mDto.setMPw(pwEn.encode(mDto.getMPw()));
    if (mDao.changeMPw(mDto)) {
        return true;
    }
    return false;
}
  • 회원가입을 암호화해서 저장했고, 로그인할 때 암호화된 비밀번호만 비교했었으니 변경해서 저장할 때도 꼭 암호화를 해서 저장해주어야 한다!
  • 처음에 암호화를 안 하고 저장해버려서 로그인 할 수 없었다 ㅠ

 

  • MemberDao (java, xml)
더보기
  • .java
boolean changeMPw(MemberDto mDto);

 

  • .xml
<update id="changeMPw">
    update member set mPw=#{mPw} where mId=#{mId}
</update>

 

  • 결과
더보기
비밀번호 공란 시
비밀번호 오류 시
비밀번호 확인이 끝난 후
변경 비밀번호를 8글자 미만, 20글자 초과로 적을 시
비밀번호 재입력에서 변경 비밀번호와 일치하지 않을 시
비밀번호 재입력에서 변경 비밀번호와 일치 시
비밀번호 변경에 성공
비밀번호 변경 성공 후

 


 

전체 피드백

  • 아직 비밀번호가 틀렸을 경우를 작업하지 않았지만 이건 나중에 작업할 수 있도록 해두고 중요하다고 했던 게시판을 먼저 해보기로 했다.

 


728x90

'프로그래밍 > +a' 카테고리의 다른 글

slPro 7차 일지  (0) 2024.01.15
slPro 6차 일지  (1) 2024.01.11
slPro 4차 일지  (1) 2024.01.08
미니 팀 프로젝트 중간 과정 (파이썬)  (1) 2024.01.03
slPro 3차 일지  (0) 2024.01.01
728x90

팀플을 하고 나서(마지막 글은 안 올렸지만 끝났다.) 주말은 푹 쉬고 오늘부터 다시 차근차근 시작하기로 했다.


1차 일지 - [ 설정 ], [ 회원가입 ]
2차 일지 - [ 로그인 ]
3차 일지 - [ index에서 로그인 상태에 따라 display none/block 제어하고 p태그 내용 바꾸기 ], [ 로그아웃 ]

4차 일지 - [ header에 회원 아이디, 회원정보/로그아웃 링크 or 비회원 문구 추가 후 로그인 상태에 따라 display none/block 제어 ], [ 일반 회원 정보 확인 ]

 

1. header - 문구 추가 후 로그인 상태에 따라 display none/block 제어

더보기
 <script type="text/javascript">
 	$(()=>{
		console.log('loginId: ${loginId}');
		let id = '${loginId}';
		if (id != ''){
			console.log('헤더) 로그인 된 아이디 있음')
			$('#loginIdHeader').html(id+"님&ensp;");
 			if (id == 'admin'){
 				$('#checkAM').html("회원 정보 확인");
 			}
 			document.getElementById("loginIdHeaderOn").style.display = 'block';
 			document.getElementById("loginIdHeaderOff").style.display = 'none';
		}else{
			console.log('헤더) 로그인 된 아이디 없음')
		}
	})
 </script>
<div style="height: 180px; background-color: #2e2e34;">
	
	<nav class="top-right">
	
		<div>
			<nav id="loginIdHeaderOn" style="padding: 15px 10px 0px 0px; float: right; display: none;">
				<p id="loginIdHeader" style="color: #fff; float: left;">test님</p>
				&ensp;<a href="/member/info" id="checkAM">내 정보 확인</a>
				&ensp;<a href="/member/logout">로그아웃</a>&ensp;
			</nav>
			<nav id="loginIdHeaderOff" style="padding: 15px 10px 0px 0px; float: right; display: block;">
				<p id="loginIdHeader" style="color: #fff; float: left;">현재 로그인 상태가 아닙니다.</p>
			</nav>
		</div>
		
		<ul class="top-menu" style="color: #d8d3cd">
			<li><a>menu1</a></li>
			<li><a>menu2</a></li>
			<li><a>menu3</a></li>
			<li><a>menu4</a></li>
		</ul>
		
	</nav>
	
</div>
  • 정보 확인을 만들기 전에 쉬운 것부터 하기 위해 이것부터 진행했다.

 

비로그인 시
일반 회원 로그인 시
관리자 로그인 시
  • 아직 index(메인화면)에서 노란박스를 변경하지 않았는데, 어떻게 변경하면 좋을지 고민중이다.

 

 

2. 정보 확인

  • MemberController.java
더보기
@GetMapping("/member/info")
public String info(Model model, HttpSession session) {
    log.info("==> GetMapping - memberInfo 요청 <==");
    return "memberInfo";
}
  • memberInfo.jsp 화면으로 넘김

 

  • memberInfo.jsp
더보기
  • 비동기 통신으로 할 필요는 없었지만, 연습 겸 비동기 통신으로 했다.

 

  • body
<div>
    <p>현재 로그인 중인 id: ${loginId}</p>

    <div id="infoPwCh" style="display: block;">
        <input type="password" name="mPw" id="infoMPw" class="input" placeholder="비밀번호를 입력하세요" maxlength="20" autocapitalize="off">
        <div id="infoCh" style="display: none;"></div>
        <button type="submit" class="btn btn-3 btn-3e" id="infoSubBtn" style="text-align: center;">비밀번호 확인</button>
    </div>
    <div id="info">
    </div>
    <br>
    <button type="button" onclick="location.href='/'">메인화면</button>
</div>

 

  • script
<script type="text/javascript">
    let loginId = '<%=(String)session.getAttribute("loginId")%>';
    $('#infoSubBtn').on('click',function(){
        let pw = $('#infoMPw').val();
        if (pw==''){
            $('#infoCh').html('비밀번호를 입력해주세요.').css('color','red');
            document.getElementById("infoCh").style.display = 'block';
            $('#infoMPw').focus();
            return;
        }
        let chPwSend = {mPw:pw};
        console.log("chPwSend: ",chPwSend);
        $.ajax({
            method:'get',	// post로 보낼 필요 없음
            url: '/member/memberInfo',
            data: chPwSend,
        }).done(function(res){
            console.log("res: ",res);
            let mDto = res;

            if(loginId=="admin"){
                $("#info").html("아직 작업 안 함!");
                document.getElementById("infoPwCh").style.display = 'none';
            }else{
                $("#info").html("<p>이름: "+mDto.mname+"</p>");
                $("#info").append("<p>이메일: "+mDto.memail+"</p>");
                if (mDto.gName!=null){
                    $("#info").append("<p>게임 닉네임: "+mDto.gname+"</p>");
                }
                $("#info").append("<p>가입 날짜: "+mDto.joinDate+"</p>");
                document.getElementById("infoPwCh").style.display = 'none';
            }

        }).fail((err,status)=>{
            console.log("err:", err);
            console.log("status:", status);
            $("#info").html("비밀번호가 틀렸습니다.");

        })
    });
</script>

 

  • MemberAsyController.java
더보기
@GetMapping("/member/memberInfo")
public MemberDto memberInfo(@RequestParam(name = "mPw") String mPw, HttpSession session) {
    log.info("==> GetMapping - memberInfo 요청 <==");
    String mId = session.getAttribute("loginId").toString();
    if (mId.equals("admin")) {
        //return mSer.mInfoAd();
    }

    return mSer.memberInfo(mId, mPw);
}
  • 주석 처리 해둔 부분은 관리자일 경우의 코드라 대충 정해놓기만 했다.

 

  • MemberService.java
더보기
public MemberDto memberInfo(String mId, String mPw) {
    log.info("===> mSer memberInfo 요청 <===");
    BCryptPasswordEncoder pwEn = new BCryptPasswordEncoder();
    MemberDto mDto = mDao.memberInfo(mId);
    if(pwEn.matches(mPw, mDto.getMPw())) {
        return mDto;
    }
    return null;
}
  • 사실 로그인 메소드를 사용하고 그 다음에 정보를 가져갈까? 했는데 그러면 불필요하게 DB를 두 번이나 가야하기 때문에 패스했다.
  • BCryptPasswordEncoder의 matches는 무조건 앞에 변형 전 문자, 뒤에 변형 후 문자가 들어간다!

 

  • MemberDao (java, xml)
더보기
  • .java
MemberDto memberInfo(String mId);

 

  • .xml
<select id="memberInfo" parameterType="String">
    select * from member where mId=#{param1}
</select>

 아직 이해하지 못했는데, 여기서 mId를 사용하니 파라미터가 없다면서 오류가 났다. 다른 것들에서는 잘 사용했는데 왜..? 반환값이 클래스라서 그런가..? 이 부분은 더 공부를 해야할 것 같다.

 

  • 결과
더보기
처음 화면
공란으로 버튼 클릭 시

 

  • 일반 회원

 

  • 관리자 (아직 안 함)

 


 

전체 피드백

  • index의 노란박스 고민) 헤더에서 해당 페이지가 무슨 페이지인지 확인 후 그것에 따라 변경하는 것이 가능하다면 헤더에 넣은 아이템들을 index에서만 저 노란박스에 보이게 하고, 그 외에는 헤더에서만 보이게 하고 싶은데 몇번 시도했으나 실패했고, 가능할지 모르겠다.
  • 비밀번호 틀렸을 때의 경우를 작업 안 했다! 작업할 것!!

 


 

728x90

'프로그래밍 > +a' 카테고리의 다른 글

slPro 6차 일지  (1) 2024.01.11
slPro 5차 일지  (0) 2024.01.09
미니 팀 프로젝트 중간 과정 (파이썬)  (1) 2024.01.03
slPro 3차 일지  (0) 2024.01.01
미니 팀 프로젝트 사전 준비 (파이썬)  (2) 2024.01.01
1 2 3 4 5 ··· 27