나는 코로나 시국에 여자친구를 만나려구 핀란드에 가게 되었다. 이전 보다 가기가 힘들었지만 틈틈히 대사관 모니터링을 통해 3가지 제출 서류 1) 코로나 음성확인서 pcr 2) 백신 확인서 3) 완치 확인서
위 세가지 서류중 2가지 가 필요했으나
Green List로 한국이 설정되어 3가지 서류중 한가지 만 가져가면 되었다.
그로 인해 나는 백신을 3차까지 맞았다는 증명서를 챙겼다. 체크인 서류를 확인 한다.
그 후 게이트에서 대기 후 약 15분 정도 지연이 된거같다.
이때 나는 상당히 긴장이 되었는데 핀란드는 상당히 많이 가보았지만
원래 8시간 비행기인데 우크라이나 때문에 경로가 변경 되어 14시간으로 비행기가 돌아간다 무려 약 비행시간이 2배나 늘었다.
마지막 전 친구가 보내준 최후의 커피를 먹고 비행기에 탄다
밤 10시 비행기라 바로 자려고 했는데..
기내식에 핀란드맥주중에 좋아하는 산델스 맥주가 나왔다.
이건 먹어야된다.
기내식은 제육볶음에 밥 조금 이렇게 나온다.
상당히 괜찮았다. 대신 out비행기 에서 기내식은 기대하면 안된다. 핀란드 식으로 나오기 떄문에 한국인 입맛에는 안맞을 수가 있다 ㅠ 나는 한번도 맞은 적이 없다.
자 보아라 경이로운 14시간 ..
밥을 먹고 다행인지 모르겠으나 코로나시국에는 항상 자리가 많아 3열을 혼자쓴다.
그래서 나는 가로로 누워 잔다. 하지만 기체가 흔들릴때마다 잠에서 깨고 비행기에서는 에어컨을 틀기 때문에 처음에는 덥더가 점점 추워진다.
일단 자자 해서 12시 부터 8시까지 푹잣다 정말 놀라웠다. 일부러 몸을 좀 피곤하게 만든것도 있지만 맥주의 힘인가 정말 푹자서 다행이다 자고 일어나니 약 4시간 정도 남아있던거 같다.
4시간 동안 나는 유튜브 + 넷플릭스에서 다운한 여러가지 콘텐츠를 보며 시간을 보냈다 .
맞다 곧 착륙 할 시간이다.
도착하니 핀란드 시간으로 새벽 6시 까마득 했다. 이때 여자친구가 데리러 오기 때문에 입국심사 등을 빠르게 끝내려고 했다.
도착 후 넘나 친절하게 한국어로 도착만 따라가면 된다.
그리고 여태까지 핀란드에서 수많은 입국 심사를 했는데 이번이 제일 심했다 무려 20분 정도 당한듯 ;;
앞에서 기다리는 사람이나 당하는 사람이나 핀란드 사람 일처리는 알아주어야 한다. 질문 하나 하고 타이핑 질문하나 하고 타이핑
나같은 경우에는 일단 아웃티켓이 3개월 뒤이기 때문에 질문 목록은 다음과 같다.
1) 어디에 가는가? - 여자친구 집에 간다. 2) 여자친구 친구 집은 어디인가? - 000 에 간다 3) 얼마나 핀란드에 머무나? - 3개월 정도 4) 아웃티켓 있냐? - ㅇㅇ 바우처 보여드림 -5) 여자친구랑 만난기간은? (이때 부터 기분이 좀 나쁘기 시작함) -n년정도 만났다. 6). 집주소는 아는가? … … 아 집주소.. 맞다 나는 집주소를 모른다.
집주소 모른다니까 ㅇㅋㅇㅋ 하고 그냥 보내줄주 알았는데 뒤에 승무원 있으니까 일단 너 옆으로 가고 승무원 먼저 해줄게 이러면서 한 10분 기달렸다. 이건 매너니까 .. ;; 승무원이 왕이지 꼬우면 내가 승무원 해야지
여자친구랑 통홰 후 집주소를 받고 주소를 보여주고 약 10가지 넘는 질문을 받고 범죄자 취급 당하며 .. 나는 입국 스탬프를 받을 수 있엇다. 나는 여태 핀란드만 in out 티켓이 약 5개가 넘는다;; 이게 제일 의외다…
우선 앞서 말하지만 나는 6년정도 회계 + 인사 부서에서 나이대비 괜찮은 연봉을 받으며 지내다 회의감에 절여져 개발자로 직무전환을 하려고 했다.
그 첫번째 단계인 국비지원 학원이 금일로써 끝낫다. 이번 처음 프로젝트를 하면서 많은 부분을 배워나갔고 생각하게 되었다.
나는 4월 1일 수료이지만 나는 3월 31일 여자친구를 보러 핀란드에 와 있다.
우선 첫 개발자로써 프로젝트라 좀 설렛던거 같다.
처음 회의는 5명의 팀끼리 학원 밑 카페에서 주제와 Wireframes를 제작 하게 되었다.
와이어프레임을 제작하고 나는 그것을 토대로 간략하게 사이트를 만들어 보았다.
약 2주간 프론트엔드 기간으로 잡고 5명의 팀원에서
팀원1 : 메인페이지 제작 팀원2 : 유저 회원가입등 제작 팀원3,4 : 어드민 페이지제작 팀원 5: 게시판제작
처음 말이 부트스트랩을 쓰냐 안쓰냐고 말이 있엇지만 부트는 쓰지 않기로 하고 나름 자부심을 느끼며 작업을 하려고 했다.
하지만 나중에 안 사실로는 부트를 쓰는게 맞았다고 본다. 확실히 전문가가 만튼 툴로 만든 사이트는 디자인에 디자도 모르는 팀원들이 감당하기에는 너무나 높은 산인듯 했다. 퀄리티 차이가 너무 심하였다. 하지만 개발자로서 처음 프로젝트 이며 내 사이트를 손수 만들어보고 싶다는 생각이 우선이 었다.
오마주사이트 없이 그냥 에타라는 사이트를 비교해가며 디자인을 해왔는데 현실감이 떨어졌엇다 .
우여곡절끝에 처음생각한 프론트 화면디자인 2주 걸리는 시간이 생각보다 절약되어 약 10일 정도에 완료 되었다.
그 뒤 서버 및 데이터베이스 세팅을 하였는데
생각보다 데이터베이스 세팅이 너무 단조롭게 진행되었다.
우리가 생각한 기능은 다음과같다.
1) 다중게시판 : 커뮤니티 이다 보니 다채로운 카테고리가 존재해야함. 2) 댓글 : 커뮤니티 소통의 기본 3) 쪽지 , 스크랩 기본 커뮤니티에 준하는 기능들 좋아요 싫어요 등 4) 관리자페이지
그리고 내가 맡은 부분은 게시판 댓글 어드민페이지 그 외 팀원들이 막히는 부분이 있으면 도와주었다. 나도 팀원들에게 많은 도움을 받았다.
그 후 나는 기존에 보여줄 것이 게시판 밖에 없어 고민 하던차에 어드민 페이지에 힘들 실어 주기로했다.
발표는 내가 맡기로 하였지만 발표시간과 출국시간의 차이로 아쉽게 발표는 하지 못했다.
어드민 페이지에서는 모든 정보를 한번에 볼수 있으며 여러가지 정보를 관리자가 한페이지에 관리했으면 한다. 내가 어드민페이지에서 가장 충실하게 만든 부분이라고 할수있다 .
기능은 다음과 같다 . 메인 페이지 : 가입회원 및 게시글이 계속 순차적으로 올라오면 원페이지에서 확인가능 회원 관리 : 모든 회원을 관리하며 가입 승인 거절 추방 대기 4가지로 관리를 한다. 그리고 이페이지에서 모달로 게시글 댓글을 볼수 있다. 데이터테이블을 활용하여 페이징등을 진행하였다. 게시판 관리 : 국내취업 해외취업 … 삭제게시판 등 약 6가지 카테고리에 중분류로 관리가 되며 데이터테이블로 소트가 가능하다. 그리고 글에 접근및 이페이지에서 삭제가 가능하다. 데이터 일별 조회 : 날자 to 날자를 선택하면 그 해당하는 날자에 대해 모든 게시판의 갯수가 나오며 구글차트로 데이터 시각화를 제공한다.
이렇게 4가지로 나누게 된다.
100퍼센트 구현을 했는데 역시나 기능상으로 아쉬운것없지만 항상 아쉬운 부분은 내생각에는 역시 디자인이다. 유튜브에서 내가 자주 보는 개발자 유튜브에서는 일단 기능이 다 좋아도 디자인이 좋아야 즉 보기가 좋아야 눈길이 간다는 것이었다.
왜? 만들때는 알지 못하였을까
이번 핀란드에서 포트폴리오가 많으면 2개 적으면 1개 정도 나올 거 같은데 일단 한번 디자인부터 싹 혼자 다시 만들어보면서 감각을 키워야 겠다.
최종 발표를 짐싸면서 줌으로 들었는데
결과적으로는 후련했다.
이렇게 나의 첫번째 프로젝트가 끝나고 다음 프로젝트는 개인 프로젝트 1개 ~2개 정도 준비 후에 핀란드에서 나가 한국에 출국하게 되면 면접보러 다녀야겠다.
public class BoardVO {
private int board_no;
private String board_title;
private String board_writer;
private String board_content;
private int board_hit;
private int board_like;
private int board_hate;
private String board_img;
private String board_img_path;
private int board_type;
private Timestamp board_regdate;
}
vo 객체 생성 뒤 페이징 및 검색을 위한 객체 생성
@Getter
@Setter
public class PageVO {
//사용자가 선택한 페이지 정보를 담을 변수.
private int pageNum;
private int countPerPage;
//검색에 필요한 데이터를 변수로 선언.
private String keyword;
private String condition;
public PageVO() {
this.pageNum = 1;
this.countPerPage = 5;
}
}
페이징을 위한 다음 이전 등 버튼 개수를 위한 클래스 제작
@Setter
@Getter
@ToString
public class PageCreator {
private PageVO paging;
private int articleTotalCount;
private int endPage;
private int beginPage;
private boolean prev;
private boolean next;
private final int buttonNum = 5;
private void calcDataOfPage() {
endPage = (int) (Math.ceil(paging.getPageNum() / (double) buttonNum) * buttonNum);
beginPage = (endPage - buttonNum) + 1;
prev = (beginPage == 1) ? false : true;
next = articleTotalCount <= (endPage * paging.getCountPerPage()) ? false : true;
if(!next) {
endPage = (int) Math.ceil(articleTotalCount / (double) paging.getCountPerPage());
}
}
//컨트롤러가 총 게시물의 개수를 PageCreator에게 전달한 직후에
//바로 페이징 버튼 알고리즘이 돌아갈 수 있도록 setter를 커스텀.
public void setArticleTotalCount(int articleTotalCount) {
this.articleTotalCount = articleTotalCount;
calcDataOfPage();
}
}
@PostMapping("/delete")
@ResponseBody
public String delete(@RequestBody int bno, HttpSession session) {
System.out.println("삭제 글 번호 : " + bno);
SnsBoardVO vo = service.getDetail(bno);
UserVO user = (UserVO) session.getAttribute("login");
//로그인을 하지 않았거나, 작성자와 현재 로그인한 id가 일치하지 않는다면
if(user == null || !user.getUserId().equals(vo.getWriter()) ) {
return "noAuth";
}
service.delete(bno);
우선 삭제할 글번호와 로그인한 세션의 정보를 가져온다.
그 후 vo 객체에서 get으로 불러 준 뒤
userVo의 세션 정보와 매핑시킨디ㅏ.
그 후 조건문으로 로그인을 하지않은 경우 에러코드를 ajax쪽으로 보낸다,
그후
//파일 객체를 생성해서 지워지고 있는 게시물의 파일을 지목
File file = new File(vo.getUploadpath()+"\\"+vo.getFilename());
System.out.println("파일 삭제완료");
return file.delete() ? "Success" : "fail"; // 파일 삭제 메소드
파일객체를 생성자로 불러와 지워 지고 있는 게시물의 파일을 지목한다.,
그후 리턴값으로 fail or success를 넣는다.
그 뒤 Service문을 작성한다.
게시물의 정보를 가져올 GetDetail() 과
삭제를 담당하게 될 getDelete를 작성한다.
//파일 객체를 생성해서 지워지고 있는 게시물의 파일을 지목
File file = new File(vo.getUploadpath()+"\\"+vo.getFilename());
System.out.println("파일 삭제완료");
return file.delete() ? "Success" : "fail"; // 파일 삭제 메소드
@Override
public void delete(int bno) {
mapper.delete(bno);
}
mapper.xml을 작성한다.
<delete id="delete">
delete from snsboard
where bno = #{bno}
</delete>
//삭제 처리
//삭제하기 링크를 클릭했을 때 이벤트를 발생 시켜서
//비동기 방식으로 삭제를 진행해 주세요. (삭제 버튼은 한 화면에 여러개 겠죠?)
//서버쪽에서 권한을 확인 해 주세요. (작성자와 로그인 중인 사용자의 id를 비교해서)
//일치하지 않으면 문자열 "noAuth" 리턴, 성공하면 "Success" 리턴.
// url: /snsBoard/delete, method: post
3) 두번째 매개변수(콜백함수)에서 JSON 파일을 이용하여 로드된 데이터를 처리한다. 콜백함수는 로드된 데이터를 인자로 넘겨받는다.(JSON 데이터를 참조하기 위해 data 변수를 사용하고 있다.)
참고하자
을 활용 하여
컨트롤러와 통신을 한다. url을 넣어주고
콜백으로
for문을 돌려 str에 html을 add 시켜준다.
그 후 컨트롤러와 통신을 하여 불러온 list의 값으로
작성자 파일이름, 파일경로 등등을 불러와 준다.
그 후 str이 완료가 되었으면
J-Qurry 함수 html()로 전달을 해준다.
그 후 reset 값이 true로 바뀌면 str의 값은 ''으로 초기화가 된다.
VO 제작
@Getter
@Setter
public class PageVO {
//사용자가 선택한 페이지 정보를 담을 변수.
private int pageNum;
private int countPerPage;
//검색에 필요한 데이터를 변수로 선언.
private String keyword;
private String condition;
public PageVO() {
this.pageNum = 1;
this.countPerPage = 10;
}
}
//게시글의 이미지 파일 전송 요청
//ResponseEntity: 응답으로 변환될 정보를 모두 담은 요소들을 객체로 만들어서 반환해 줍니다.
@GetMapping("/display")
public ResponseEntity<byte[]> getFile(String fileLoca, String fileName) {
System.out.println("fileName: " + fileName);
System.out.println("fileLoca: " + fileLoca);
File file = new File("C:\\Users\\mls00\\OneDrive\\바탕 화면\\upload\\" + fileLoca + "\\" + fileName);
System.out.println(file);
ResponseEntity<byte[]> result = null;
try {
HttpHeaders headers = new HttpHeaders();
//probeContentType: 파라미터로 전달받은 파일의 타입을 문자열로 변환해 주는 메서드.
//사용자에게 보여주고자 하는 데이터가 어떤 파일인지를 검사해서 응답 상태 코드를 다르게 리턴할 수도 있습니다.
headers.add("Content-Type", Files.probeContentType(file.toPath()));
// headers.add("merong", "hello");
//ResponseEntity<>(응답 객체에 담을 내용, 헤더에 담을 내용, 상태 메세지);
//FileCopyUtils: 파일 및 스트림 데이터 복사를 위한 간단한 유틸리티 메서드의 집합체.
//file객체 안에 있는 내용을 복사해서 byte배열로 변환해서 바디에 담아 화면에 전달.
result = new ResponseEntity<>(FileCopyUtils.copyToByteArray(file), headers, HttpStatus.OK);
} catch (IOException e) {
e.printStackTrace();
}
return result;
}
$(function() {
//등록하기 버튼 클릭 이벤트
$('#uploadBtn').click(function() {
regist();
});
//등록을 담당하는 함수
function regist() {
//세션에서 현재 로그인 중인 사용자 정보(아이디)를 얻어오자
const user_id = '${sessionScope.login.userId}';
//자바스크립트의 파일 확장자 체크 검색.
let file = $('#file').val();
console.log(user_id);
console.log(file);
//.을 제거한 확장자만 얻어낸 후 그것을 소문자로 일괄 변경
file = file.slice(file.indexOf('.') + 1).toLowerCase();
console.log(file);
if(file !== 'jpg' && file !== 'png' && file !== 'jpeg' && file !== 'bmp') {
alert('이미지 파일(jpg, png, jpeg, bmp)만 등록이 가능합니다.');
$('#file').val('');
return;
} else if(user_id === '') { //세션 데이터가 없다 -> 로그인 x
alert('로그인이 필요한 서비스입니다.');
return;
}
세션데이터가 없을경우 글 등록을 하지 못하게 하였다.
그리고 얻어온 세션 아이디로 글을 올린 사람에 대한 정보를 쿼리문으로 넘겨 줄 것이다.
우선 file이라는 변수로 file의 값을 가져온다.
그 후 파일 업로드가 끝나면 slice 함수로 .png 등 확장자를 잘라내어
tolowerCase로 소문자로 변경해준다.
그 뒤 이미지 파일만 올릴 수 있게
if문 으로 검증 로직을 구현 해준다.
즉 로그인 안됨 = 파일을 올릴 수없음
로그인 함 = 이미지 파일만 올릴 수있음
이 두가지 관문을 거쳐야 파일을 올릴 수있게 처리를 하였다.
미리보기 기능
function readURL(input) {
if (input.files && input.files[0]) {
var reader = new FileReader(); //비동기처리를 위한 파읽을 읽는 자바스크립트 객체
//readAsDataURL 메서드는 컨텐츠를 특정 Blob 이나 File에서 읽어 오는 역할 (MDN참조)
reader.readAsDataURL(input.files[0]);
//파일업로드시 화면에 숨겨져있는 클래스fileDiv를 보이게한다
$(".fileDiv").css("display", "block");
reader.onload = function(event) { //읽기 동작이 성공적으로 완료 되었을 때 실행되는 익명함수
$('#fileImg').attr("src", event.target.result);
console.log(event.target)//event.target은 이벤트로 선택된 요소를 의미
}
}
}
$("#file").change(function() {
readURL(this); //this는 #file자신 태그를 의미
});
1) 이미지가 1개인 경우
미리보기 이미지가 표시될 이미지 태그를 생성하고,input file태그를 생성하고, 자바스크립트의FileReader()를 통해 이미지가 로딩되면 이미지 태그의src속성이 교체되도록 합니다.
2. 컨트롤러 제작
@GetMapping("/snsList")
public void snsList() {}
@PostMapping("/upload")
@ResponseBody
public String upload(MultipartFile file, String content,
HttpSession session) {
try {
String writer = ((UserVO)session.getAttribute("login")).getUserId();
//날짜별로 폴더를 생성해서 파일을 관리
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd");
Date date = new Date();
String fileLoca = sdf.format(date);
//저장할 폴더 경로
String uploadPath = "C:\\Users\\mls00\\OneDrive\\바탕 화면\\upload\\" + fileLoca;
File folder = new File(uploadPath);
if(!folder.exists()) {
folder.mkdir(); //폴더가 존재하지 않는다면 생성해라.
}
String fileRealName = file.getOriginalFilename();
//파일명을 고유한 랜덤 문자로 생성.
UUID uuid = UUID.randomUUID();
String uuids = uuid.toString().replaceAll("-", "");
//확장자를 추출합니다.
String fileExtension = fileRealName.substring(fileRealName.indexOf("."), fileRealName.length());
System.out.println("저장할 폴더 경로: " + uploadPath);
System.out.println("실제 파일명: " + fileRealName);
System.out.println("폴더명: " + fileLoca);
System.out.println("확장자: " + fileExtension);
System.out.println("고유랜덤문자: " + uuids);
String fileName = uuids + fileExtension;
System.out.println("변경해서 저장할 파일명: " + fileName);
//업로드한 파일을 서버 컴퓨터의 지정한 경로 내에 실제로 저장.
File saveFile = new File(uploadPath + "\\" + fileName);
file.transferTo(saveFile);
//DB에 insert 작업을 진행.
SnsBoardVO snsVO = new SnsBoardVO(0, writer, uploadPath, fileLoca, fileName, fileRealName, content, null);
service.insert(snsVO);
return "success";
} catch (Exception e) {
System.out.println("업로드 중 에러 발생: " + e.getMessage());
return "fail"; //에러가 났을 시에는 실패 키워드를 반환.
}
}
@GetMapping("/snsList") void 처리로 페이지 이동을 할 수 있게 처리
로그인 세션을 가져와 로그인한 아이디의 값을 가져온다.
날자별로 폴더를 생성해서 관리를 하려고한다.
자바 객체인 SimpleDateFormat을 활용하여
날자를 생성해주고
Date date = new Date(); 하면 오늘 날자가 생성된다.
fileloca라는 변수에 저장해 둔다 (20220215) 이런식으로 나옴
저장할 폴더의 경로를 선택해준다.
String uploadPath = 실제사용할경로 + fileloca
그 후 자바 FIle객체를 사용하여
exist() - File 객체가 참조하는 파일 또는 디렉토리가 실제로 존재하면 true를, 그렇지
않으면 false를 리턴한다.
메소드를 활용하여 파일이 한번 생성 된 뒤 중복 생성을 방지한다.
String fileRealName = file.getOriginalFilename();로 파일 고유의 이름을 추출