์ฌํ๊ฐ๋ค์ ์ํ ์น์๋น์ค
์์ฐ์์
์ ์๋๊ธฐ
ํ๋ก์ ํธ ์ด๋ฆ๊ณผ ๊ฐ์ด 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 |