- 간단한 소개: 간단한 작품 소개
- 사용 기술: 프로젝트에서 사용한 기술 스택
- 결과: 프로젝트의 주요 성과 또는 학습한 점
- 다이어그램: 프로젝트를 다이어그램으로 표현
- 링크: 프로젝트 관련 GitHub 링크
이 사이트는 오목 매칭 사이트입니다. https://ok5pj.com/login
구글 로그인, 채팅, 전적확인, 닉네임 변경, 오목 매칭이 가능합니다.
먼저 가장 기본인 로그인한 화면입니다.
이건 매칭된 상태입니다.
오목의 룰중 렌주룰을 적용 구현하였습니다.
승판이 났을 경우
전체 구성
programming language | JAVA |
IDE | IntelliJ IDEA |
Framework | JAVA SPRING |
Cloud DataBase | MongoDB Atlas, AWS Dynamo DB |
Server | Nginx |
Security | JWT, OAuth 2.0 |
Build Tool | Gradle |
Deployment | AWS Elastic Beanstalk |
Library
dependencies { //라이브러리 넣는곳
//lombok
compileOnly ("org.projectlombok:lombok")
annotationProcessor("org.projectlombok:lombok")
//thymeleaf
implementation("org.springframework.boot:spring-boot-starter-thymeleaf")
//DynamoDB
implementation(platform("software.amazon.awssdk:bom:2.20.85"))
implementation("software.amazon.awssdk:dynamodb-enhanced")
//Maper
implementation ("org.modelmapper:modelmapper:2.4.4")
//security
implementation ("org.springframework.boot:spring-boot-starter-security")
implementation ("org.thymeleaf.extras:thymeleaf-extras-springsecurity6:latest.release")
implementation ("org.springframework.security:spring-security-test")
//JWT
implementation ("io.jsonwebtoken:jjwt:0.9.1")
implementation ("javax.xml.bind:jaxb-api:2.3.1")
//OAuth2
implementation ("org.springframework.boot:spring-boot-starter-oauth2-client")
//jpa
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
//mustache
implementation("org.springframework.boot:spring-boot-starter-mustache")
//web
implementation("org.springframework.boot:spring-boot-starter-web")
//start test
testImplementation("org.springframework.boot:spring-boot-starter-test")
//Mongo DB
implementation("org.springframework.boot:spring-boot-starter-data-mongodb-reactive")
implementation ("org.springframework.boot:spring-boot-starter-webflux")
//web socket
implementation("org.springframework.boot:spring-boot-starter-websocket")
//date -> json
implementation ("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.13.0")
}
DataBase(MongoDB Atlas)
MongoDB Atlas DataBase Collections 구성
game | chat |
game
오목판을 저장하기 위한 Collections 입니다.
플레이어가 흑돌을 두었다면 세션 통신을 통하여 백돌에게 신호를 주기 때문에 game Collections 는 사실상 필요 없을것 같지만,
만약 게임 진행중 플레이어가 사이트를 나갔다 들어왔을 경우 game Collections의 Data를 불러와 게임을 이어서 플레이 할 수 있습니다.
chat
채팅 내역을 저장하기 위한 Collections 입니다.
Collections game구성
Key | 설명 |
id | 고유 값 부여(저장시간+uesr1,user2) |
user1 | 흑돌을 플레이 할 유저의 Email을 저장 |
user2 | 백돌을 플레이 할 유저의 Email을 저장 |
table | 진행 중인 오목판 저장 |
tableLog |
오목판 로그를 저장 |
turn | 누구의 턴인지 저장 |
time | 오목 제한시간 저장 |
game Entity 구성
@Data
@Document(collection = "game")
public class Game_E {
@Id
private String id;
@Indexed
private String user1;
@Indexed
private String user2;
@Indexed
private String[][] table;
@Indexed
List<String [][]> tableLog;
@Indexed
private String turn;
@Indexed
private String time;
}
DataBase(AWS Dynamo DB)
AWS Dynamo DB DataBase Table구성
Account | Connect | RefreshToken |
Account
회원의 계정 정보를 저장하기 위한 저장하기 위한 Table입니다.
Connect
회원의 접속에 대한 정보를 저장합니다.
실시간 데이터 스트림 기능을 사용했습니다.
만약 Connect Table의 connect data가 바뀐다면 접속한 모든 유저에게 data를 보냅니다.
RefreshToken
JWT방식의 인증 방식을 사용함으로 RefreshToken을 저장하는 Table입니다.
Account Table 구성
Column | 설명 |
id | 랜덤한 숫자를 이용한 고유 ID |
Google Email을 저장 | |
nickname | Google 닉네임을 저장 |
profile_url | Google 프로필 URL을 저장 |
subnuckname | 오목 사이트에 사용할 nickname을 저장 |
victory | 승리 횟수 저장 |
defeat | 패배 횟수 저장 |
Connect Table 구성
Column | 설명 |
Google Email을 저장 | |
connect | 0,1,2,3으로 이루어졌다. 순서대로 미접속, 접속, 매칭중, 게임중으로 나뉜다. |
matching | 매칭버튼을 클릭 했을 시간을 저장, 매칭이 되었을 경우 매칭 시간으로 흑돌과 백돌을 결정함 |
position | Mongo DB에 Collections인 game의 Primary Key를 저장한다. |
socket | 접속시 생성되는 Socket ID를 저장한다. |
RfreshToken Table 구성
Column | 설명 |
id | 고유한 id값을 저장한다. |
refresh_token | 헤더, 페이로드, 서명(HS256)을 포함한 JWT 토큰을 저장한다. |
user_id | Account Table에 id값을 저장한다. |
핵심 로직
세션
최초 접속시 세션 저장 및 Dyanmo DB Table에 connect값 변경
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
// Principal 대신에 attributes에서 username 가져오기
String username = (String) session.getAttributes().get("username");
System.out.println("afterConnectionEstablished: username: " + username);
Connect_E connectE = connectService.loadByEmail(username);
if (connectE == null) {
connectE = new Connect_E();
connectE.setConnect("1");
connectE.setMatching(0);
connectE.setEmail(username);
connectE.setSocket(session.getId());
connectService.save(connectE);
} else {
connectE.setConnect("1");
connectE.setMatching(0);
connectE.setSocket(session.getId());
connectService.updateConnect(connectE);
}
sessions.add(session);
socketMap.put(session.getId(), session);
System.out.println("WebSocket connection Open. Session ID: " + session.getId() + " // Username: " + username);
}
렌주룰 확인 및 돌 카운트 로직
public Map<String, String> countingLock1(int countBean, int left, int leftDiagonal, int up, int upRightDiagonal, int row, int col, int wayCount, String[] point, String[][] table, String color) {
switch (wayCount) {
case 0:
row--;
if (isInBounds(table, row, col) && checkLockThere(table, row, col, color)) {
left++;
return countingLock1(countBean, left, leftDiagonal, up, upRightDiagonal, row, col, wayCount, point, table, color);
} else if (isInBounds(table, row, col) && countBean < 1 && !checkLockThere(table, row, col, "White")) {
countBean++;
return countingLock1(countBean, left, leftDiagonal, up, upRightDiagonal, row, col, wayCount, point, table, color);
} else if (isInBounds(table, row, col) && checkLockThere(table, row, col, "White")) {
countBean = 0;
left = 0;
return countingLock1(countBean, left, leftDiagonal, up, upRightDiagonal, Integer.parseInt(point[0]), Integer.parseInt(point[1]), 1, point, table, color);
} else {
countBean = 0;
return countingLock1(countBean, left, leftDiagonal, up, upRightDiagonal, Integer.parseInt(point[0]), Integer.parseInt(point[1]), 4, point, table, color);
}
case 1:
row--;
col--;
if (isInBounds(table, row, col) && checkLockThere(table, row, col, color)) {
leftDiagonal++;
return countingLock1(countBean, left, leftDiagonal, up, upRightDiagonal, row, col, wayCount, point, table, color);
} else if (isInBounds(table, row, col) && countBean < 1 && !checkLockThere(table, row, col, "White")) {
countBean++;
return countingLock1(countBean, left, leftDiagonal, up, upRightDiagonal, row, col, wayCount, point, table, color);
} else if (isInBounds(table, row, col) && checkLockThere(table, row, col, "White")) {
countBean = 0;
leftDiagonal = 0;
return countingLock1(countBean, left, leftDiagonal, up, upRightDiagonal, Integer.parseInt(point[0]), Integer.parseInt(point[1]), 2, point, table, color);
} else {
countBean = 0;
return countingLock1(countBean, left, leftDiagonal, up, upRightDiagonal, Integer.parseInt(point[0]), Integer.parseInt(point[1]), 5, point, table, color);
}
case 2:
col--;
if (isInBounds(table, row, col) && checkLockThere(table, row, col, color)) {
up++;
return countingLock1(countBean, left, leftDiagonal, up, upRightDiagonal, row, col, wayCount, point, table, color);
} else if (isInBounds(table, row, col) && countBean < 1 && !checkLockThere(table, row, col, "White")) {
countBean++;
return countingLock1(countBean, left, leftDiagonal, up, upRightDiagonal, row, col, wayCount, point, table, color);
} else if (isInBounds(table, row, col) && checkLockThere(table, row, col, "White")) {
countBean = 0;
up = 0;
return countingLock1(countBean, left, leftDiagonal, up, upRightDiagonal, Integer.parseInt(point[0]), Integer.parseInt(point[1]), 3, point, table, color);
} else {
countBean = 0;
return countingLock1(countBean, left, leftDiagonal, up, upRightDiagonal, Integer.parseInt(point[0]), Integer.parseInt(point[1]), 6, point, table, color);
}
case 3:
col--;
row++;
if (isInBounds(table, row, col) && checkLockThere(table, row, col, color)) {
upRightDiagonal++;
return countingLock1(countBean, left, leftDiagonal, up, upRightDiagonal, row, col, wayCount, point, table, color);
} else if (isInBounds(table, row, col) && countBean < 1 && !checkLockThere(table, row, col, "White")) {
countBean++;
return countingLock1(countBean, left, leftDiagonal, up, upRightDiagonal, row, col, wayCount, point, table, color);
} else if (isInBounds(table, row, col) && checkLockThere(table, row, col, "White")) {
upRightDiagonal = 0;
Map<String, String> result = new HashMap<>();
result.put("left", Integer.toString(left));
result.put("leftDiagonal", Integer.toString(leftDiagonal));
result.put("up", Integer.toString(up));
result.put("upRightDiagonal", Integer.toString(upRightDiagonal));
return result;
} else {
countBean = 0;
return countingLock1(countBean, left, leftDiagonal, up, upRightDiagonal, Integer.parseInt(point[0]), Integer.parseInt(point[1]), 7, point, table, color);
}
case 4:
row++;
if (isInBounds(table, row, col) && checkLockThere(table, row, col, color)) {
left++;
return countingLock1(countBean, left, leftDiagonal, up, upRightDiagonal, row, col, wayCount, point, table, color);
} else if (isInBounds(table, row, col) && countBean < 1 && !checkLockThere(table, row, col, "White")) {
countBean++;
return countingLock1(countBean, left, leftDiagonal, up, upRightDiagonal, row, col, wayCount, point, table, color);
} else if (isInBounds(table, row, col) && checkLockThere(table, row, col, "White")) {
left = 0;
countBean = 0;
return countingLock1(countBean, left, leftDiagonal, up, upRightDiagonal, Integer.parseInt(point[0]), Integer.parseInt(point[1]), 1, point, table, color);
} else {
countBean = 0;
return countingLock1(countBean, left, leftDiagonal, up, upRightDiagonal, Integer.parseInt(point[0]), Integer.parseInt(point[1]), 1, point, table, color);
}
case 5:
row++;
col++;
if (isInBounds(table, row, col) && checkLockThere(table, row, col, color)) {
leftDiagonal++;
return countingLock1(countBean, left, leftDiagonal, up, upRightDiagonal, row, col, wayCount, point, table, color);
} else if (isInBounds(table, row, col) && countBean < 1 && !checkLockThere(table, row, col, "White")) {
countBean++;
return countingLock1(countBean, left, leftDiagonal, up, upRightDiagonal, row, col, wayCount, point, table, color);
} else if (isInBounds(table, row, col) && checkLockThere(table, row, col, "White")) {
leftDiagonal = 0;
countBean = 0;
return countingLock1(countBean, left, leftDiagonal, up, upRightDiagonal, Integer.parseInt(point[0]), Integer.parseInt(point[1]), 2, point, table, color);
} else {
countBean = 0;
return countingLock1(countBean, left, leftDiagonal, up, upRightDiagonal, Integer.parseInt(point[0]), Integer.parseInt(point[1]), 2, point, table, color);
}
case 6:
col++;
if (isInBounds(table, row, col) && checkLockThere(table, row, col, color)) {
up++;
return countingLock1(countBean, left, leftDiagonal, up, upRightDiagonal, row, col, wayCount, point, table, color);
} else if (isInBounds(table, row, col) && countBean < 1 && !checkLockThere(table, row, col, "White")) {
countBean++;
return countingLock1(countBean, left, leftDiagonal, up, upRightDiagonal, row, col, wayCount, point, table, color);
} else if (isInBounds(table, row, col) && checkLockThere(table, row, col, "White")) {
up = 0;
countBean = 0;
return countingLock1(countBean, left, leftDiagonal, up, upRightDiagonal, Integer.parseInt(point[0]), Integer.parseInt(point[1]), 3, point, table, color);
} else {
countBean = 0;
return countingLock1(countBean, left, leftDiagonal, up, upRightDiagonal, Integer.parseInt(point[0]), Integer.parseInt(point[1]), 3, point, table, color);
}
case 7:
row--;
col++;
if (isInBounds(table, row, col) && checkLockThere(table, row, col, color)) {
upRightDiagonal++;
return countingLock1(countBean, left, leftDiagonal, up, upRightDiagonal, row, col, wayCount, point, table, color);
} else if (isInBounds(table, row, col) && countBean < 1 && !checkLockThere(table, row, col, "White")) {
countBean++;
return countingLock1(countBean, left, leftDiagonal, up, upRightDiagonal, row, col, wayCount, point, table, color);
} else if (isInBounds(table, row, col) && checkLockThere(table, row, col, "White")) {
upRightDiagonal = 0;
Map<String, String> result = new HashMap<>();
result.put("left", Integer.toString(left));
result.put("leftDiagonal", Integer.toString(leftDiagonal));
result.put("up", Integer.toString(up));
result.put("upRightDiagonal", Integer.toString(upRightDiagonal));
return result;
} else {
Map<String, String> result = new HashMap<>();
result.put("left", Integer.toString(left));
result.put("leftDiagonal", Integer.toString(leftDiagonal));
result.put("up", Integer.toString(up));
result.put("upRightDiagonal", Integer.toString(upRightDiagonal));
return result;
}
}
return null;
}
사용 예시
public void board3x3Check(String[][] table, String[] point, Map<String, String> map, Map<String, Object> msg) {
int point1 = Integer.parseInt(point[0]);
int point2 = Integer.parseInt(point[1]);
int rowM = Math.max(point1 - 6, 0);
int rowP = Math.min(point1 + 6, table.length - 1);
int colM = Math.max(point2 - 6, 0);
int colP = Math.min(point2 + 6, table[0].length - 1);
int key = 0;
int key1 = 0;
for (int row = rowM; row <= rowP; row++) {
for (int col = colM; col <= colP; col++) {
if ("0".equals(table[row][col]) || "3".equals(table[row][col])) {
int x33Count = 0;
boolean x66Count = false;
Map<String, String> lockCount_2S = countingLock1(0, 0, 0, 0, 0, row, col, 0, new String[]{Integer.toString(row), Integer.toString(col)}, table, "Black");
Map<String, String> lockCount_6S = countingLock(1,1,1,1,row, col,0,new String[]{Integer.toString(row), Integer.toString(col)},table,"Black");
if ("2".equals(lockCount_2S.get("left"))) {
System.out.println("left2");
x33Count++;
}
if ("2".equals(lockCount_2S.get("leftDiagonal"))) {
System.out.println("leftDiagonal2");
x33Count++;
}
if ("2".equals(lockCount_2S.get("up"))) {
System.out.println("up2");
x33Count++;
}
if ("2".equals(lockCount_2S.get("upRightDiagonal"))) {
System.out.println("upRightDiagonal2");
x33Count++;
}
if (Integer.parseInt(lockCount_6S.get("left")) >= 6 ||
Integer.parseInt(lockCount_6S.get("leftDiagonal")) >=6 ||
Integer.parseInt(lockCount_6S.get("up")) >= 6 ||
Integer.parseInt(lockCount_6S.get("upRightDiagonal")) >= 6) {
map.put("TthreeRow" + key, Integer.toString(row));
map.put("TthreeCol" + key, Integer.toString(col));
msg.put("TthreeRow" + key, Integer.toString(row));
msg.put("TthreeCol" + key, Integer.toString(col));
x66Count = true;
}
if (x33Count >= 2) {
table[row][col] = "3";
map.put("TthreeRow" + key, Integer.toString(row));
map.put("TthreeCol" + key, Integer.toString(col));
msg.put("TthreeRow" + key, Integer.toString(row));
msg.put("TthreeCol" + key, Integer.toString(col));
key++;
System.out.println("@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@33입니다.");
} else if ("3".equals(table[row][col]) && !x66Count) {
table[row][col] = "0";
map.put("DTthreeRow" + key1, Integer.toString(row));
map.put("DTthreeCol" + key1, Integer.toString(col));
msg.put("DTthreeRow" + key1, Integer.toString(row));
msg.put("DTthreeCol" + key1, Integer.toString(col));
key1++;
System.out.println("@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@33이 아니게 됐습니다..");
}
} else if ("1".equals(table[row][col])) {
}
}
}
}
승패시 상대방에게 승패 여부 전송
public void UserDefeatAddOrSocketSend(String userName) throws JsonProcessingException {
Map<String, String> msg = new HashMap<>();
Connect_E connectE = connectService.loadByEmail(userName);
User_E userE = userDetailService.loadUserByUsername(userName);
userE.setDefeat(userE.getDefeat() + 1);
connectE.setConnect("1");
connectService.updateConnect(connectE);
userDetailService.update(userE);
msg.put("type", "deFeat");
webSocketHandler.sendMessageToTarget(objectMapper.writeValueAsString(msg),connectE.getSocket());
gameService.findById(connectE.getPosition())
.flatMap(data -> gameService.delete(data))
.subscribe();
}
public void UserVictoryAddOrSocketSend(String userName) throws JsonProcessingException {
Map<String, String> msg = new HashMap<>();
Connect_E connectE = connectService.loadByEmail(userName);
connectE.setConnect("1");
connectService.updateConnect(connectE);
User_E userE = userDetailService.loadUserByUsername(userName);
userE.setVictory(userE.getVictory() + 1);
userDetailService.update(userE);
msg.put("type", "vicTory");
webSocketHandler.sendMessageToTarget(objectMapper.writeValueAsString(msg),connectE.getSocket());
}
채팅 전송 로직
@PostMapping("/api/chat")
public Mono<Chat_E> sendMsg(@RequestBody Map<String, Object> chatE, Principal principal) {
Chat_E chat_e = new Chat_E();
User_E userE = userDetailService.loadUserByUsername(principal.getName());
Flux<Game_E> gameUser1 = mongDBGameRepository.findByUser1(principal.getName());
Flux<Game_E> gameUser2 = mongDBGameRepository.findByUser2(principal.getName());
chat_e.setMsg((String) chatE.get("msg"));
chat_e.setCreateAt(LocalDateTime.now());
if(userE.getSubnickname() != null){
chat_e.setSender(userE.getSubnickname());
}else{
chat_e.setSender(principal.getName());
}
Mono<Void> gameUser1Processing = gameUser1.collectList()
.flatMap(list -> {
if (list.isEmpty()) {
return Mono.empty();
} else {
return gameUser1.flatMap(game -> {
String userSocket = dynamoConnectRepository.findByEmail(game.getUser2()).getSocket();
chat_e.setReceiver(game.getUser2());
try {
Map<String, Object> chatMap = objectMapper.convertValue(chat_e, Map.class);
chatMap.put("type", "msg");
String jsonMessage = objectMapper.writeValueAsString(chatMap);
webSocketHandler.sendMessageToTarget(jsonMessage, userSocket);
} catch (JsonProcessingException e) {
return Mono.error(e);
}
return Mono.empty();
}).then();
}
});
Mono<Void> gameUser2Processing = gameUser2.collectList()
.flatMap(list -> {
if (list.isEmpty()) {
return Mono.empty();
} else {
return gameUser2.flatMap(game -> {
String userSocket = dynamoConnectRepository.findByEmail(game.getUser1()).getSocket();
chat_e.setReceiver(game.getUser1());
try {
Map<String, Object> chatMap = objectMapper.convertValue(chat_e, Map.class);
chatMap.put("type", "msg");
String jsonMessage = objectMapper.writeValueAsString(chatMap);
webSocketHandler.sendMessageToTarget(jsonMessage, userSocket);
} catch (JsonProcessingException e) {
return Mono.error(e);
}
return Mono.empty();
}).then();
}
});
return Mono.when(gameUser1Processing, gameUser2Processing)
.then(Mono.defer(() -> {
return chatService.saveChat(chat_e);
}));
}
매칭중인 다른 유저 찾는 로직
@GetMapping("/api/matchingfind")
public List<Connect_E> matchingUserFind(Principal principal) {
String connect = connectService.loadByEmail(principal.getName()).getConnect();
if (!Objects.equals(connect.trim(), "2")) {
return null;
}
List<Connect_E> connectEs = connectService.loadByConnect("2");
Connect_E opconnectE = new Connect_E();
Connect_E meconnectE = new Connect_E();
List<Connect_E> connect_es = new ArrayList<>();
opconnectE.setMatching(9999999);
for (Connect_E connectE1 : connectEs) {
if (opconnectE.getMatching() > connectE1.getMatching() && !Objects.equals(principal.getName(), connectE1.getEmail())) {
opconnectE = connectE1;
}
if (Objects.equals(principal.getName(), connectE1.getEmail())) {
meconnectE = connectE1;
}
}
connect_es.add(meconnectE);
connect_es.add(opconnectE);
return connect_es;
}
게임 생성 로직
@PostMapping("/api/firstgamesave")
public Mono<Game_E> firstgamesave(@RequestBody Map<String, Object> requestData, Principal principal) {
Connect_E connectE1 = connectService.loadByEmail(principal.getName());
Map<String, Object> opconnectE = (Map<String, Object>) requestData.get("opconnectE");
Connect_E connectE2 = connectService.loadByEmail((String)opconnectE.get("email"));
Game_E gameE = new Game_E();
if ((Integer) connectE1.getMatching() < (Integer) opconnectE.get("matching")) {
gameE.setUser1(connectE1.getEmail());
gameE.setUser2(opconnectE.get("email").toString());
} else {
return null;
}
String gameID = LocalDateTime.now().toString() + connectE1.getEmail() + connectE2.getEmail();
gameE.setTurn("0");
gameE.setTime("15");
gameE.setId(gameID);
gameE.setTable(getTable19n19());
gameE.setTableLog(new ArrayList<>());
connectE1.setPosition(gameID);
connectE1.setConnect("3");
connectE2.setPosition(gameID);
connectE2.setConnect("3");
connectService.updateConnect(connectE1);
connectService.updateConnect(connectE2);
return gameService.save(gameE);
}
게임중인지 확인하는 로직
@GetMapping("/api/gamefind")
public Mono<Game_E> gameFindByUser(Principal principal) {
System.out.println("gamefind");
// 이메일로 Connect_E 객체 로드
Connect_E connectE = connectService.loadByEmail(principal.getName());
return gameService.findById(connectE.getPosition());
}
배포 다이어그램
시퀀스 다이어그램
클래스 다이어그램
github: https://github.com/asdd2557/yProject
Project URI: https://ok5pj.com/login
이상입니다.
감사합니다.