SmsService
@Service
@RequiredArgsConstructor
public class SmsService {
public void sendSms() {
JsonObject params = new JsonObject();
JsonArray messages = new JsonArray();
JsonObject msg = new JsonObject();
JsonArray toList = new JsonArray();
toList.add("01099403102");
msg.add("to", toList);
msg.addProperty("from", "01099403102");
msg.addProperty("text",
"안녕하세요 줍깅입니다. 오늘 참여하실[같이 줍깅해요!]모임의 모임날짜가 3일 남으셨습니다.");
messages.add(msg);
params.add("messages", messages);
Call<GroupModel> api = APIInit.getAPI().sendMessages(APIInit.getHeaders(), params);
api.enqueue(new Callback<GroupModel>() {
@Override
public void onResponse(Call<GroupModel> call, Response<GroupModel> response) {
// 성공 시 200이 출력됩니다.
if (response.isSuccessful()) {
System.out.println("statusCode : " + response.code());
GroupModel body = response.body();
System.out.println("groupId : " + body.getGroupId());
System.out.println("status: " + body.getStatus());
System.out.println("count: " + body.getCount().toString());
} else {
try {
System.out.println(response.errorBody().string());
} catch (IOException e) {
e.printStackTrace();
}
}
}
@Override
public void onFailure(Call<GroupModel> call, Throwable throwable) {
throwable.printStackTrace();
}
});
}
}
util / coolsms / GroupModel
@Getter
public class GroupModel {
private ArrayList<JsonObject> log;
private JsonObject agent;
private JsonObject count;
private String accountId;
private String apiVersion;
private String groupId;
private String dateCreated;
private String dateUpdated;
private String _id;
private String status;
}
util / coolsms / CoolsmsMsgV4
(Coolsms서버와의 통신) public interface CoolsmsMsgV4 {
// 심플 메시지
@POST("messages/v4/send")
Call<GroupModel> sendMessage(@Header("Authorization") String auth,
@Body Message message);
// 여러건 발송
@POST("messages/v4/send-many")
Call<GroupModel> sendMessages(@Header("Authorization") String auth,
@Body JsonObject messages);
util / coolsms / config.ini
(api key, api secret 이 담겨져있는 파일)[AUTH]
api_key = {api_key 넣기}
api_secret = {api_secret 넣기}
[SERVER]
domain = api.coolsms.co.kr
protocol = https
prefix =
util / coolsms / APIInit
(헤더를 만들어서 요청하는 로직, 서버에로 보내는 로직 )public class APIInit {
private static Retrofit retrofit;
private static CoolsmsMsgV4 messageService;
public static String getHeaders() {
try {
//실험결과 절대경로만되고 상대경로는 되지않음
Ini ini = new Ini(new File("C:\\Users\\user\\Desktop\\gits\\joopging\\src\\main\\java\\com\\project\\joopging\\util\\coolsms\\config.ini"));
String apiKey = ini.get("AUTH","api_key");
String apiSecret =ini.get("AUTH","api_secret");
String salt = UUID.randomUUID().toString().replaceAll("-","");
String date = ZonedDateTime.now(ZoneId.of("Asia/Seoul")).toString().split("\\[")[0];
Mac sha256_HMAC = Mac.getInstance("HmacSHA256");
SecretKeySpec secret_key = new SecretKeySpec(apiSecret.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
sha256_HMAC.init(secret_key);
String signature = new String(Hex.encodeHex(sha256_HMAC.doFinal((date + salt).getBytes(StandardCharsets.UTF_8))));
return "HMAC-SHA256 ApiKey=" + apiKey + ", Date=" + date + ", salt=" + salt + ", signature=" + signature;
} catch (InvalidKeyException | NoSuchAlgorithmException | IOException e) {
e.printStackTrace();
}
return null;
}
public static CoolsmsMsgV4 getAPI() {
if (messageService == null) {
setRetrofit();
messageService = retrofit.create(CoolsmsMsgV4.class);
}
return messageService;
}
public static void setRetrofit() {
HttpLoggingInterceptor interceptor = new HttpLoggingInterceptor();
// Request 시 로그가 필요하면 추가하세요.
// interceptor.setLevel(HttpLoggingInterceptor.Level.HEADERS);
// interceptor.setLevel(HttpLoggingInterceptor.Level.BODY);
String domain = "api.coolsms.co.kr";
String protocol = "https";
String prefix = "/";
try {
//실험결과 절대경로만되고 상대경로는 되지않음
Ini ini = new Ini(new File("C:\\Users\\user\\Desktop\\gits\\joopging\\src\\main\\java\\com\\project\\joopging\\util\\coolsms\\config.ini"));
if (ini.get("SERVER", "domain") != null) domain = ini.get("SERVER", "domain");
if (ini.get("SERVER", "protocol") != null) protocol = ini.get("SERVER", "protocol");
if (ini.get("SERVER", "prefix") != null) prefix = ini.get("SERVER", "prefix");
} catch (IOException e) {
e.printStackTrace();
}
OkHttpClient client = new OkHttpClient.Builder()
.addInterceptor(interceptor)
.build();
retrofit = new Retrofit.Builder()
.baseUrl(protocol + "://" + domain + prefix)
.addConverterFactory(ScalarsConverterFactory.create())
.addConverterFactory(GsonConverterFactory.create())
.client(client)
.build();
}
}
schedule / SmsSchedule
추가로직
//스케쥴러 매일 9시
//러닝데이트 1일 전에 알럿문자메세지
@Scheduled(cron = "0 0 9 * * *")
@Transactional(readOnly = true)
public void sendRunningDateAlertToCrew() {
List<Post> postList = postRepository.findAll();
JsonArray toList = new JsonArray();
String nowPlusOneDay = getLocalDateTimeNowToStringDay(LocalDateTime.now().plusDays(1));
for (Post post : postList) {
String runningDate = getRunningDateToStringDay(post);
if (nowPlusOneDay.equals(runningDate)) {
List<Crew> crewList = post.getCrew();
String postTitle = post.getTitle();
String message = "안녕하세요 줍깅입니다. " +
"신청하신"+ " [" + postTitle + "] " +"모임의 모임날짜가 하루전입니다.";
for (Crew crew : crewList) {
User user = crew.getUserJoin();
Long userId = user.getId();
User myUser = userRepository.findById(userId).orElseThrow(
NullPointerException::new
);
String userNumber = myUser.getNumber();
toList.add(userNumber);
}
sendSms(toList, message);
}
}
}
//휴대폰 번호 인증
@Transactional(readOnly = true)
public CertificateNumberResponseDto certificatePhoneNumber(PhoneNumberRequestDto requestDto) {
JsonArray toList = new JsonArray();
String phoneNumber = requestDto.getPhoneNumber();
toList.add(phoneNumber);
Random random = new Random();
String numStr = "";
for(int i = 0; i < 4; i++) {
String ran = Integer.toString(random.nextInt(10));
numStr+=ran;
}
log.info("수신자 번호 :" + phoneNumber);
log.info("인증번호 :" + numStr);
String message = "안녕하세요 줍깅입니다. 인증번호는 [ " + numStr + " ] 입니다";
sendSms(toList, message);
CertificateNumberResponseDto responseDto = new CertificateNumberResponseDto();
responseDto.setCertificationNumber(numStr);
return responseDto;
}
schedule / SmsSchedule
추가로직
//스케쥴러 5분마다 체크
//Crew 전체에게 후기 쓰고 설문조사 유도 알럿문자메세지
@Scheduled(cron = "0 0/5 * * * *")
@Transactional(readOnly = true)
public void sendInduceReviewAlertToCrew() {
System.out.println("스케쥴러 시작");
List<Post> postList = postRepository.findAll();
JsonArray toList = new JsonArray();
String now = getLocalDateTimeNowToStringMinute(LocalDateTime.now());
for (Post post : postList) {
String runningDate = getRunningDateToStringMinute(post);
if (now.equals(runningDate)) {
List<Crew> crewList = post.getCrew();
String postTitle = post.getTitle();
String message = "안녕하세요 줍깅입니다. 이번" +" ["+ postTitle + "] " + "모임은 어떠셨나요?" +
"후기를 작성하여 다른 사용자에게 플로깅이 얼마나 좋은지 알려주세요!" +
"[이벤트] 이벤트 기간 중 설문조사를 작성하시면 소정의 기프티콘을 드려요! " +
"https://forms.gle/X3nQmmbHiRwmmWtZ8";
for (Crew crew : crewList) {
User user = crew.getUserJoin();
Long userId = user.getId();
User myUser = userRepository.findById(userId).orElseThrow(
NullPointerException::new
);
String userNumber = myUser.getNumber();
toList.add(userNumber);
}
sendSms(toList,message);
}
}
}
schedule / SmsSchedule
추가로직
//스케쥴러 5분마다 체크
//CrewHead 에게 출석체크 url 알럿문자메세지
@Scheduled(cron ="0 0/5 * * * *")
@Transactional(readOnly = true)
public void sendAttendanceCheckAlertToCrewHead() {
System.out.println("스케쥴러 시작");
List<Post> postList = postRepository.findAll();
JsonArray toList = new JsonArray();
String now = getLocalDateTimeNowToStringMinute(LocalDateTime.now());
for (Post post : postList) {
String runningDate = getRunningDateToStringMinute(post);
if (now.equals(runningDate)) {
User user = post.getWriter();
Long userId = user.getId();
User myUser = userRepository.findById(userId).orElseThrow(
NullPointerException::new
);
String number = myUser.getNumber();
String postTitle = post.getTitle();
Long postId = post.getId();
String message = "안녕하세요 줍깅입니다." +" ["+ postTitle +"] "+ "모임의 모임원들은" +
"다 모이셨나요? 출석체크를 해주세요!" +
"출석체크는 앞으로 유저간의 신뢰도를 측정하는데 도움이 됩니다!" +
"http://joopgging.link/meetingcheck/" + postId;
toList.add(number);
sendSms(toList,message);
}
}
}
처음 구현하려한 의도는 노쇼나 출석에 관한 각종 사고를 방지함에 있어 표현이 너무 직설적이지 않은 뱃지를 부여함으로 출석률을 높이려는 의도였다.
스케쥴러를 돌리는 주기는 유저 입장으로써 뱃지의 기준을 도달했을 즉시 알림으로 알려주고 주는것이 가장 좋겠지만 러닝커브를 생각해 fixedDelay = 5000
(로직이 돌고난 5초후) 간격으로
빠르게 업데이트를 해주었다.
기본으로 출석체크를 누르면 false값이 유저에게 저장됨으로 단순하게 출석률을 계산할 경우 유저는 처음 모임참가를 눌렀을때 부터 지각생이라는 출석률이 저조하다는의미에 뱃지를 부여받게 된다.
//출석률 계산
//출석체크 default 값이 false 라서 2번째 모임신청을 누르는 순간 출석률이 50%가 되서 바로 지각생 뱃지를 받게됨으로
//출석 5번이상 경우의 수 (ex. 출석 5 + default 1 = 83.3% (안받음), 출석5 결석1 + default 1 = 71.4% (최저 안받음))
//결석을 2회이상 경우의 수(ex. 출석1 결석1 + default 1 = 33% (받야야함) , 출석2 + default 1 = 66%(안받음))
if (countAttendanceTrue > 5 || countAttendanceFalse > 2) {
Double attendanceRate =
(double) (countAttendanceTrue / (countAttendanceTrue + countAttendanceFalse) * 100);
System.out.println(attendanceRate);
//출석률 70프로 이하일때 지각생 뱃지
Badge badAttendanceRateBadge = Badge.of(5, 5, user);
if (attendanceRate <= 70) {
if (categoryList.contains(badAttendanceRateBadge.getCategory())) {
} else {
badgeList.add(badAttendanceRateBadge);
badgeRepository.save(badAttendanceRateBadge);
}
} else {
Badge DubBadge = badgeRepository.findByUserBadgeAndCategoryAndLevel(user,5,5);
badgeList.remove(DubBadge);
badgeRepository.delete(DubBadge);
}
} else {
}
스케줄러를 사용하면서 어려웠던 부분들
요약 : 스케줄러를 적용하면서 어려웠던점은 저희 프로젝트의 특성에 맞게 REST API를 수정하는 것과 주기 설정이 였습니다. REST API는 기능선별을 한 후 테스트코드를 작성하여 하나하나 오류를 지워나가는 방법으로 해결을 하였습니다. 주기에 대한 설정은 sms의 경우 유저 flow 의 경우의 수, 운용 자금을 생각해 결정하였고 뱃지는 도입의도에 부합하게 실시간으로 업데이트 되야한다고 판단하여 스케줄러가 두번돌아가는 것을 방지하기 위해 이전 기능이 실행된 후 1초 주기로 모든 유저를 체크하여 뱃지를 부여하는 방식으로 결정하였습니다. (쓰레드 풀이 겹치지 않게)