์ฌํ๊ฐ๋ค์ ์ํ ์น์๋น์ค
์์ฐ์์
์ ์๋๊ธฐ
ํ๋ก์ ํธ ์ด๋ฆ๊ณผ ๊ฐ์ด For your travel (๋น์ ์ ์ฌํ์ ์ํ) ์น ์๋น์ค๋ฅผ ์ ์ํ๊ณ ์ ํ์์ต๋๋ค.
์ฌํ๊ฐ๋ค์ด ์ ํฌ ์๋น์ค๋ฅผ ํตํด ๊ด๊ด์ง ์ ๋ณด์ ๋ค๋ฅธ ์ฌ์ฉ์๋ค์ ์ฌํํ๊ธฐ๋ฅผ ์ฐธ๊ณ ํ์ฌ ์ฑ๊ณต์ ์ธ ์ฌํ๊ณํ์ ์์ฑํ๋๋ฐ ๋์์ ๋ฐ์ ์ ์์ต๋๋ค.
์ฌ์ฉ๊ธฐ์
- Vue.js
- vuex
- vuex-persistedstate
- axios
- vue-router
- vue-wordcloud
- jwt-decode
- SpringBoot
- myBatis
- jjwt
- spring-boot-starter-mail
- MySql
๊ตฌํ๊ธฐ๋ฅ
- ์ฌ์ฉ์ ํ๋กํ
- ํ๋ก์ฐ, ํ๋ก์
- ๋ก๊ทธ์ธ, ๋ก๊ทธ์์, ํ์๊ฐ์ (jwt)
- ๊ฒ์ํ CRUD, ๋๊ธ CRUD
- ํ์์ ๋ณด ์์
1. ๋ก๊ทธ์ธ, ๋ก๊ทธ์์, ํ์๊ฐ์ (jwt, vuex, vuex-persistedstate)
1-1. ํ์๊ฐ์
SpringBoot
- ํ์๊ฐ์ Controller
@RestController
@RequestMapping("/user")
@Api("์ฌ์ฉ์ ์ปจํธ๋กค๋ฌ API V1")
@CrossOrigin("*")
public class UserController {
private final Logger logger = LoggerFactory.getLogger(UserController.class);
@Value("${file.path}")
private String uploadPath;
@Value("${file.imgPath}")
private String uploadImgPath;
private UserService us;
private JwtService js;
private EmailService es;
@Autowired
public UserController(UserService us, JwtService js,EmailService es) {
super();
this.us = us;
this.js = js;
this.es = es;
}
...
@ApiOperation(value = "ํ์๊ฐ์
", notes = "ํ์๊ฐ์
(์ด๋ฏธ์งO)", response = Map.class)
@PostMapping("/join-form")
public ResponseEntity<Map<String, Object>> join(
@ModelAttribute @ApiParam(value = "ํ์๊ฐ์
ํ ํ์์ ์ ๋ณด", required = true) UserDto userDto,
HttpServletRequest request) throws IllegalStateException, IOException {
MultipartFile file = userDto.getFile();
System.out.println(file);
logger.debug("join user : {} ", userDto);
Map<String, Object> resultMap = new HashMap<>();
HttpStatus status = HttpStatus.ACCEPTED;
if (!file.isEmpty()) {
String today = new SimpleDateFormat("yyMMdd").format(new Date());
String saveFolder = uploadPath + File.separator + today;
logger.debug("์ ์ฅ ํด๋ : {}", saveFolder);
File folder = new File(saveFolder);
if (!folder.exists())
folder.mkdirs();
String originalFileName = file.getOriginalFilename();
if (!originalFileName.isEmpty()) {
String saveFileName = UUID.randomUUID().toString()
+ originalFileName.substring(originalFileName.lastIndexOf('.'));
userDto.setSfolder(today);
userDto.setOfile(originalFileName);
userDto.setSfile(saveFileName);
logger.debug("์๋ณธ ํ์ผ ์ด๋ฆ : {}, ์ค์ ์ ์ฅ ํ์ผ ์ด๋ฆ : {}", file.getOriginalFilename(), saveFileName);
file.transferTo(new File(folder, saveFileName));
}
}
try {
us.joinUser(userDto);
resultMap.put("code", "200");
status = HttpStatus.ACCEPTED;
} catch (Exception e) {
logger.error("์ ๋ณด์กฐํ ์คํจ : {}", e);
resultMap.put("code", "500");
status = HttpStatus.ACCEPTED;
}
return new ResponseEntity<Map<String, Object>>(resultMap, status);
}
@ApiOperation(value = "ํ์๊ฐ์
", notes = "ํ์๊ฐ์
(์ด๋ฏธ์งX)", response = Map.class)
@PostMapping("/join-json")
public ResponseEntity<Map<String, Object>> joinJson(@RequestBody @ApiParam(value = "ํ์๊ฐ์
ํ ํ์์ ์ ๋ณด", required = true) UserDto userDto,
HttpServletRequest request) throws IllegalStateException, IOException {
logger.debug("join user : {} ", userDto);
Map<String, Object> resultMap = new HashMap<>();
HttpStatus status = HttpStatus.ACCEPTED;
try {
userDto.setOfile("noimg.png");
userDto.setSfile("noimg.png");
userDto.setSfolder("noimg");
us.joinUser(userDto);
resultMap.put("code", "200");
status = HttpStatus.ACCEPTED;
} catch (Exception e) {
logger.error("์ ๋ณด์กฐํ ์คํจ : {}", e);
resultMap.put("code", "500");
status = HttpStatus.ACCEPTED;
}
return new ResponseEntity<Map<String, Object>>(resultMap, status);
}
...
ํ์๊ฐ์
์งํ ์ ํ๋กํ ์ด๋ฏธ์ง๋ฅผ ๋ฃ๋ ๊ฒฝ์ฐ์ ๋ฃ์ง ์๋ ๊ฒฝ์ฐ๋ฅผ ์ฒ๋ฆฌํ๊ธฐ ์ํด /join-form
๊ณผ /join-json
๋ ๊ฐ์ ์ปจํธ๋กค๋ฌ๋ก ๊ตฌ๋ถํ์์ต๋๋ค.
- ํ์๊ฐ์ service
import java.security.MessageDigest;
import java.security.SecureRandom;
@Service
public class UserServiceImpl implements UserService {
private static final int SALT_SIZE = 16;
private UserMapper UserMapper;
public UserServiceImpl(UserMapper UserMapper) {
super();
this.UserMapper = UserMapper;
}
@Override
public void joinUser(UserDto UserDto) throws Exception {
String salt = makeSalt();
UserDto.setPassword(hashing(UserDto.getPassword(),salt));
UserDto.setSalt(salt);
UserMapper.joinUser(UserDto);
}
private String makeSalt() throws Exception {// salt๊ฐ ์์ฑ
SecureRandom random = new SecureRandom();
byte[] temp = new byte[SALT_SIZE];
random.nextBytes(temp);
return byteToString(temp);
}
// ๋น๋ฐ๋ฒํธ ํด์ฑ
private String hashing(String password, String salt) throws Exception {
MessageDigest md = MessageDigest.getInstance("SHA-256");// SHA-256 ํด์ํจ์๋ฅผ ์ด์ฉ
for (int i = 0; i < 10000; i++) {// salt๋ฅผ ํฉ์ณ ์๋ก์ด ํด์๋น๋ฐ๋ฒํธ๋ฅผ ์์ฑํด ๋์ฝ๋ฉ๋ฅผ ๋ฐฉ์ง
String temp = password + salt;// ํจ์ค์๋์ Salt๋ฅผ ํฉ์ณ ์๋ก์ด ๋ฌธ์์ด ์์ฑ
md.update(temp.getBytes());// temp์ ๋ฌธ์์ด์ ํด์ฑํ์ฌ md์ ์ ์ฅ
password = byteToString(md.digest());// md๊ฐ์ฒด์ ๋ค์ด์ ์คํธ๋ฅผ ์ป์ด password๋ฅผ ๊ฐฑ์
}
return password;
}
private String byteToString(byte[] temp) {// ๋ฐ์ดํธ ๊ฐ์ 16์ง์๋ก ๋ณ๊ฒฝ
StringBuilder sb = new StringBuilder();
for (byte a : temp) {
sb.append(String.format("%02x", a));
}
return sb.toString();
}
import java.security.MessageDigest
import java.security.SecureRandom
๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ด์ฉํ์ฌ ๋น๋ฐ๋ฒํธ๋ฅผ ์ํธํ ํ์ฌ db์ ์ ์ฅํ์์ต๋๋ค.
๋ํ mybatis๋ฅผ ํ์ฉํ์ฌ db์ ์ ๊ทผํ์์ต๋๋ค.
MyBatis
- Java Object์ SQL๋ฌธ ์ฌ์ด์ ์๋ Mapping ๊ธฐ๋ฅ์ ์ง์ํ๋ ORM Framework
- MyBatis๋ SQL์ ๋ณ๋์ ํ์ผ(XML)๋ก ๋ถ๋ฆฌํด์ ๊ด๋ฆฌ
- ์๋ฐ์ SQL์ ์ฐ๊ฒฐ๋ง ํด์ค๋ค!
- JAVA - mybatis - MySQL
Vue
- formData๋ฅผ ํ์ฉํ์ฌ ์ด๋ฏธ์ง๋ฅผ ์๋ฒ๋ก post ํ๊ธฐ ์ํด์ header์
Content-Type
์multipart/form-data
๋ก ์ค์ ํด์คฌ์ต๋๋ค
const formData = new FormData();
formData.append("userId", this.user.userId);
formData.append("userName", this.user.userName);
formData.append("password", this.user.password);
formData.append("email", this.user.email);
formData.append("file", this.user.file);
axios({
method: 'post',
url: `${process.env.VUE_APP_BASE_URL}user/join-form`,
headers: {
'Content-Type': 'multipart/form-data'
},
data: formData
})
.then(({ data }) => {
if (data.code === "200") {
alert("ํ์๊ฐ์
์ด ์๋ฃ๋์์ต๋๋ค.");
this.$router.push({ name: "home" });
}
})
.catch(() => alert("์ ์ ํ ์๋ํด ์ฃผ์ธ์"));
}
1-2. ๋ก๊ทธ์ธ, ์ฌ์ฉ์์ธ์ฆ
SpringBoot
- ๋ก๊ทธ์ธ Controller
@ApiOperation(value = "๋ก๊ทธ์ธ", notes = "Access-token๊ณผ ๋ก๊ทธ์ธ ๊ฒฐ๊ณผ ๋ฉ์ธ์ง๋ฅผ ๋ฐํํ๋ค.", response = Map.class)
@PostMapping("/login")
public ResponseEntity<Map<String, Object>> login(@RequestBody @ApiParam(value = "๋ก๊ทธ์ธ ์ ํ์ํ ํ์์ ๋ณด(์์ด๋, ๋น๋ฐ๋ฒํธ)", required = true) UserDto userDto) {
Map<String, Object> resultMap = new HashMap<>();
HttpStatus status = null;
try {
UserDto loginUser = us.loginUser(userDto);
if (loginUser != null) {
JwtResponseDTO jrdto = new JwtResponseDTO();
jrdto.setAuthority(loginUser.getAuthority());
jrdto.setUserId(loginUser.getUserId());
jrdto.setUserNo(loginUser.getUserNo());
String accessToken = js.createAccessToken("user", jrdto);// key, data
String refreshToken = js.createRefreshToken("user", jrdto);// key, data
us.saveRefreshToken(loginUser.getUserNo(),refreshToken);
logger.debug("๋ก๊ทธ์ธ accessToken ์ ๋ณด : {}", accessToken);
logger.debug("๋ก๊ทธ์ธ refreshToken ์ ๋ณด : {}", refreshToken);
resultMap.put("access-token", accessToken);
resultMap.put("refresh-token", refreshToken);
resultMap.put("code", "200");
status = HttpStatus.ACCEPTED;
} else {
resultMap.put("code", "401");
status = HttpStatus.ACCEPTED;
}
} catch (Exception e) {
logger.error("๋ก๊ทธ์ธ ์คํจ : {}", e);
resultMap.put("code", "500");
status = HttpStatus.ACCEPTED;
}
return new ResponseEntity<Map<String, Object>>(resultMap, status);
}
์ฐ์ service๋จ์ ๋ก๊ทธ์ธ ์์ฒญ์ ๋ณด๋ ๋๋ค.
๋ก๊ทธ์ธ ์์ฒญ์ ๋ฐ๋ service๋จ
@Override
public UserDto loginUser(UserDto loginUser) throws Exception {
String userId = loginUser.getUserId();
String salt = UserMapper.getSalt(userId);
loginUser.setPassword(hashing(loginUser.getPassword(),salt));
return UserMapper.loginUser(loginUser);
}
ํด๋ผ์ด์ธํธ์์ ๋๊ฒจ์ค ๋น๋ฐ๋ฒํธ์ db์ ์ ์ฅ๋์๋ salt๊ฐ์ ์ด์ฉํ์ฌ ๋น๋ฐ๋ฒํธ๋ฅผ ํด์ฑํด์คฌ์ต๋๋ค. (db์ password์ปฌ๋ผ์๋ ์ํธํ๋ ๋ฐ์ดํฐ๊ฐ ์ ์ฅ๋์ด ์๊ธฐ ๋๋ฌธ์)
ํด์ฑ๋ ๋น๋ฐ๋ฒํธ๋ก ์
๋ฐ์ดํธ๋ loginUser dto๋ฅผ mapper์ ์ ๋ฌํ์ฌ ๋ก๊ทธ์ธ์ ์งํํ๊ฒ ๋ฉ๋๋ค.
JwtResponseDto ๋ฅผ JwtService(import io.jsonwebtoken.Claims
import io.jsonwebtoken.Jws
import io.jsonwebtoken.Jwts
import io.jsonwebtoken.SignatureAlgorithm
๋ฅผ ์ด์ฉํ๊ณ ์๋ Serviceํด๋์ค)์ ๋๊ฒจ์ค์ refreshToken๊ณผ accessTokenn์ ์์ฑํ๊ฒ ๋ฉ๋๋ค.
์ด๋ ๊ฒ ์์ฑ๋ token์ ํด๋ผ์ด์ธํธ์๊ฒ response๋ก ๋๊ฒจ์ค๋๋ค.
Vue
- ๋ก๊ทธ์ธ
TheLogin.vue
...mapActions(userStore, ["userConfirm", "getUserInfo"]),
login() {
if (this.saveid) {
this.setId(this.user.userId);
} else {
this.deleteId();
}
let msg = "";
if (!this.user.userId || this.userId === "") {
msg = "์์ด๋๋ฅผ ์
๋ ฅํด์ฃผ์ธ์.";
}
if (!this.user.password || this.user.password === "") {
msg = "๋น๋ฐ๋ฒํธ๋ฅผ ์
๋ ฅํด์ฃผ์ธ์.";
}
if (msg !== "") {
alert(msg);
return;
}
this.userConfirm(this.user);
}
vuex์ userConfirm action์ ์ ๋ ฅ๋ฐ์ user๊ฐ์ฒด๋ฅผ ๋๊ฒจ์ค๋๋ค.
userStore.js
(vuex)
userConfirm({ commit, dispatch }, user) {
axios({
method: "post",
url: `${process.env.VUE_APP_BASE_URL}user/login`,
data: user,
})
.then(({ data }) => {
if (data.code === "200") {
let accessToken = data["access-token"];
let refreshToken = data["refresh-token"];
// console.log("login success token created!!!! >> ", accessToken, refreshToken);
commit("SET_IS_LOGIN", true);
commit("SET_IS_LOGIN_ERROR", false);
commit("SET_IS_VALID_TOKEN", true);
sessionStorage.setItem("access-token", accessToken);
sessionStorage.setItem("refresh-token", refreshToken);
dispatch("getUserInfo");
alert("๋ก๊ทธ์ธ ์ฑ๊ณต :)");
router.push({ name: "home" }).catch(() => {});
} else {
alert("๋ก๊ทธ์ธ ์คํจ! ๋น๋ฐ๋ฒํธ๋ฅผ ํ์ธํด์ฃผ์ธ์");
commit("SET_IS_LOGIN", false);
commit("SET_IS_LOGIN_ERROR", true);
commit("SET_IS_VALID_TOKEN", false);
}
})
.catch((err) => {
console.log(err);
});
}
๋ก๊ทธ์ธ ์ฑ๊ณต ์ state๊ฐ๊ณผ token๊ฐ์ sessionStorage์ ์ ์ฅํฉ๋๋ค. ๋ํ ์ฌ์ฉ์ ์ ๋ณด๋ฅผ ๊ฐฑ์ ํ๊ธฐ ์ํด getUserInfo
action์ ์ํํฉ๋๋ค.
vuex-persistedstate
๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ํตํด ํ๋ฉด ์๋ก๊ณ ์นจ ์์๋ vuex์ store๊ฐ์ด ์ฌ๋ผ์ง์ง ์๋๋ก ์ฒ๋ฆฌํ์ต๋๋ค.
2. ์ฌ์ฉ์ ํ๋กํ
Vue
๋ฐ์ํ์ผ๋ก ํ๋กํ์ ๋ด์ฉ์ด ๋ฐ๋๋๋ก ํ๊ธฐ์ํด์ computed์ watch๋ฅผ ์ฌ์ฉํ์ต๋๋ค.
computed: {
...mapState(userStore, ["isLogin", "userInfo", "followings"]),
userImgSrc() {
return `${process.env.VUE_APP_BASE_URL}file/download/${this.profileInfo.sfolder}/${this.profileInfo.ofile}/${this.profileInfo.sfile}`;
},
userId() {
return this.$route.params.userId;
}, // param์ผ๋ก userId๋ฅผ ๊ฐ์ ธ์ด
followerCnt() {
return this.follower.length;
},
followingCnt() {
return this.following.length;
},
},
watch: {
userId() {
axios
.get(`${process.env.VUE_APP_BASE_URL}user/info/${this.userId}`)
.then(({ data }) => {
this.profileInfo = data.userInfo;
axios
.get(
`${process.env.VUE_APP_BASE_URL}travel-review/list-user/${this.profileInfo.userNo}`
)
.then(({ data }) => {
this.travelReview = data.travelReview;
})
.catch(() => {
console.log("review ๊ฐ์ ธ์ค๋ ์ค ์๋ฌ ๋ฐ์");
});
axios
.get(
`${process.env.VUE_APP_BASE_URL}travel-plan/planlist-preview/${this.profileInfo.userNo}`
)
.then(({ data }) => {
this.plans = data.planlist;
})
.catch(() => {
console.log("planlist ๊ฐ์ ธ์ค๋ ์ค ์๋ฌ ๋ฐ์");
});
axios
.get(`${process.env.VUE_APP_BASE_URL}follow/follower/${this.profileInfo.userNo}`)
.then(({ data }) => {
this.follower = data.follower;
})
.catch(() => {
console.log("follower ๊ฐ์ ธ์ค๋ ์ค ์๋ฌ ๋ฐ์");
});
axios
.get(`${process.env.VUE_APP_BASE_URL}follow/following/${this.profileInfo.userNo}`)
.then(({ data }) => {
this.following = data.following;
})
.catch(() => {
console.log("following ๊ฐ์ ธ์ค๋ ์ค ์๋ฌ ๋ฐ์");
});
if (this.profileInfo.userNo === this.userInfo.userNo) {
// ๋ด ํ๋กํ์ธ ๊ฒฝ์ฐ
this.notMe = false; // ๋ด๊ฐ ๋ง์ผ๋ฏ๋ก notMe = false
this.followBtn = false; // follow ๋ฒํผ ๋ณด์ด๋ฉด ์๋จ
} else if (this.followings.includes(this.profileInfo.userNo)) {
this.notMe = true; // ๋ด๊ฐ ์๋, notMe = true
this.followBtn = false; // follow๋ฒํผ false
} else {
this.followBtn = true; // ํ๋ก์ฐ ๋ฒํผ ๋ณด์ฌ์ง๊ณ ,
this.notMe = false; // ๋ด๊ฐ ์๋
}
})
.catch(({ err }) => {
console.log(err);
});
},
},
1. ๋ผ์ฐํฐ์ param์ผ๋ก userId๋ฅผ ๋๊ฒจ ๋ฐ๋๋ฐ, ์ด userId๋ฅผ computed์ ๋ฑ๋ก
computed๋ ๋ฐ์ํ getter๋ผ๊ณ ์๊ฐํ๋ฉด ๋๋ค. ์ฆ this.$route.params.userId ๋ฅผ ๊ฐ์ํ๊ณ ์๋ค๊ฐ ๊ฐ์ด ๋ณ๊ฒฝ๋๋ฉด userId๋ฅผ ๋ค์ ๊ณ์ฐํ๋ค.
2. computed์ ๋ฑ๋กํ userId๋ฅผ watch์ ๋ฑ๋ก
computed๊ฐ ์ ํ๋กํผํฐ๋ฅผ ์์ฑํ๊ณ ๊ทธ๊ฒ์ getter ๋ก ์ต๋ช ํจ์๋ฅผ ์ค์ ๋๋ ๊ฒ๊ณผ๋ ๋ฌ๋ฆฌ watch๋ ์๋ฌด ํ๋กํผํฐ๋ ์์ฑํ์ง ์๊ณ ์ต๋ช ํจ์๋ ๋จ์ํ ์ฝ๋ฐฑํจ์๋ก์ ์ญํ ์ ํ๋ค. watch์ ๋ช ์๋ ํ๋กํผํฐ๋ ๊ฐ์ํ ๋์์ ์๋ฏธํ ๋ฟ์ด๋ค.
3. ์ฌ์ฉ์ ์ ๋ณด๋ฅผ ์ ๋๋ก ๋ฐ์์ค๋ฉด ์ฌํ๋ฆฌ๋ทฐ, ์ฌํ๊ณํ, ํ๋ก์, ํ๋ก์ฐ ์ ๋ณด๋ฅผ axiosํ๋๋ก ์ฝ๋๋ฅผ ์์ฑ
'๐ํ๋ก์ ํธ:Project' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
TAB(Take a Bus) (0) | 2023.08.31 |
---|---|
ํผ์ค๋์ปฌ๋ฌ ์ง๋จ ํ๋ก์ ํธ (0) | 2023.05.03 |
SignLanguage (0) | 2023.05.03 |