diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..7244c65 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,73 @@ +name: Build and Deploy to EC2 + +# 워크플로우가 언제 실행될 것인지 조건 명시 +on: + push: # 모든 브랜치 대상 + +# AWS 관련 값 변수로 설정 +env: + AWS_REGION: ap-northeast-2 + AWS_S3_BUCKET: devcard-deploy-bucket + AWS_CODE_DEPLOY_APPLICATION: Devcard-Application-CD + AWS_CODE_DEPLOY_GROUP: Devcard-Deployment-Group + +jobs: + deploy: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + steps: + - uses: actions/checkout@v4 + + # JDK 21 설치 + - name: Set up JDK 21 + uses: actions/setup-java@v4 + with: + java-version: '21' + distribution: 'temurin' + + # 환경 변수를 사용하여 application-secret.properties 파일 생성 + - name: make application-secret.properties + run: | + mkdir -p ./src/main/resources + cd ./src/main/resources + touch ./application-secret.properties + echo "kakao.javascript.key=${{ secrets.KAKAO_JAVASCRIPT_KEY }}" >> ./application-secret.properties + echo "DB_USERNAME=${{ secrets.DB_USERNAME }}" >> ./application-secret.properties + echo "DB_PASSWORD=${{ secrets.DB_PASSWORD }}" >> ./application-secret.properties + echo "GH_CLIENT_ID=${{ secrets.GH_CLIENT_ID }}" >> ./application-secret.properties + echo "GH_CLIENT_SECRET=${{ secrets.GH_CLIENT_SECRET }}" >> ./application-secret.properties + echo "GH_REDIRECT_URI=${{ secrets.GH_REDIRECT_URI }}" >> ./application-secret.properties + + # 권한 부여 + - name: Grant execute permission for gradlew + run: chmod +x ./gradlew + shell: bash + + - name: Build and Test + env: + DB_USERNAME: ${{ secrets.DB_USERNAME }} + DB_PASSWORD: ${{ secrets.DB_PASSWORD }} + run: ./gradlew build test --info + + # 빌드 파일을 zip 형식으로 압축 + - name: Make zip file + run: zip -r ./$GITHUB_SHA.zip . + shell: bash + + # AWS 권한 + - name: AWS credential 설정 + uses: aws-actions/configure-aws-credentials@v1 + with: + aws-region: ${{ env.AWS_REGION }} + aws-access-key-id: ${{ secrets.CICD_ACCESS_KEY }} + aws-secret-access-key: ${{ secrets.CICD_SECRET_KEY }} + + # S3 버킷에 빌드파일(zip 파일)을 업로드 + - name: Upload to S3 + run: aws s3 cp --region ap-northeast-2 ./$GITHUB_SHA.zip s3://$AWS_S3_BUCKET/$GITHUB_SHA.zip + + # EC2 인스턴스에 S3에 저장되어 있던 zip 파일을 받아와 배포 시작 + - name: EC2에 배포 + run: aws deploy create-deployment --application-name ${{ env.AWS_CODE_DEPLOY_APPLICATION }} --deployment-config-name CodeDeployDefault.AllAtOnce --deployment-group-name ${{ env.AWS_CODE_DEPLOY_GROUP }} --s3-location bucket=$AWS_S3_BUCKET,key=$GITHUB_SHA.zip,bundleType=zip diff --git a/appspec.yml b/appspec.yml new file mode 100644 index 0000000..fa35b32 --- /dev/null +++ b/appspec.yml @@ -0,0 +1,17 @@ +version: 0.0 +os: linux + +files: + - source: / + destination: /home/ubuntu/app + overwrite: yes + +permissions: + - object: / + owner: ubuntu + group: ubuntu + +hooks: + ApplicationStart: + - location: scripts/deploy.sh + timeout: 60 diff --git a/build.gradle b/build.gradle index f67eefe..67da93f 100644 --- a/build.gradle +++ b/build.gradle @@ -28,6 +28,7 @@ dependencies { implementation 'com.google.zxing:javase:3.5.1' implementation 'org.springframework.boot:spring-boot-starter-validation' implementation 'org.springframework.boot:spring-boot-starter-websocket' + implementation 'mysql:mysql-connector-java:8.0.33' runtimeOnly 'com.h2database:h2' testImplementation 'org.springframework.boot:spring-boot-starter-test' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' diff --git a/scripts/deploy.sh b/scripts/deploy.sh new file mode 100644 index 0000000..3a45dbc --- /dev/null +++ b/scripts/deploy.sh @@ -0,0 +1,29 @@ +#!/bin/bash + +# 빌드 파일 탐색 및 이름 설정 +BUILD_JAR=$(ls /home/ubuntu/app/build/libs/*-SNAPSHOT.jar) +JAR_NAME=$(basename $BUILD_JAR) +echo ">>> build 파일명: $JAR_NAME" >> /home/ubuntu/deploy.log + +# 빌드 파일 복사 +echo ">>> build 파일 복사" >> /home/ubuntu/deploy.log +DEPLOY_PATH=/home/ubuntu/app/ +cp $BUILD_JAR $DEPLOY_PATH + +# 현재 실행 중인 애플리케이션 종료 +echo ">>> 현재 실행중인 애플리케이션 pid 확인 후 일괄 종료" >> /home/ubuntu/deploy.log +CURRENT_PID=$(pgrep -f $JAR_NAME) + +if [ -z "$CURRENT_PID" ]; then + echo ">>> 현재 실행 중인 애플리케이션이 없습니다." >> /home/ubuntu/deploy.log +else + echo ">>> 실행 중인 애플리케이션 종료: $CURRENT_PID" >> /home/ubuntu/deploy.log + kill -15 $CURRENT_PID + sleep 5 +fi + +# 애플리케이션 배포 +DEPLOY_JAR=$DEPLOY_PATH$JAR_NAME +echo ">>> DEPLOY_JAR 배포" >> /home/ubuntu/deploy.log +echo ">>> $DEPLOY_JAR을 실행합니다" >> /home/ubuntu/deploy.log +nohup java -jar $DEPLOY_JAR >> /home/ubuntu/deploy.log 2>&1 & diff --git a/src/main/java/com/devcard/devcard/auth/config/SecurityConfig.java b/src/main/java/com/devcard/devcard/auth/config/SecurityConfig.java index 2d9bbaf..f9dbbb6 100644 --- a/src/main/java/com/devcard/devcard/auth/config/SecurityConfig.java +++ b/src/main/java/com/devcard/devcard/auth/config/SecurityConfig.java @@ -33,7 +33,13 @@ SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { "/js/**", "/images/**", "/h2-console/**", - "/walletList" + "/walletList", + "/qrcodes/**", + "/notice", + "/notice/**", + "/qna", + "/qna/**", + "/shared/**" ).permitAll(); // 그 외의 모든 요청은 인증 필요 auth.anyRequest().authenticated(); diff --git a/src/main/java/com/devcard/devcard/auth/controller/page/MypageController.java b/src/main/java/com/devcard/devcard/auth/controller/page/MypageController.java new file mode 100644 index 0000000..b19dac2 --- /dev/null +++ b/src/main/java/com/devcard/devcard/auth/controller/page/MypageController.java @@ -0,0 +1,22 @@ +package com.devcard.devcard.auth.controller.page; + +import com.devcard.devcard.auth.entity.Member; +import com.devcard.devcard.auth.model.OauthMemberDetails; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.GetMapping; + +@Controller +public class MypageController { + + @GetMapping("/mypage") + public String mypage(@AuthenticationPrincipal OauthMemberDetails oauthMemberDetails, Model model) { + if (oauthMemberDetails != null) { + Member member = oauthMemberDetails.getMember(); + model.addAttribute("member", member); + } + return "mypage"; + } + +} \ No newline at end of file diff --git a/src/main/java/com/devcard/devcard/auth/controller/LoginController.java b/src/main/java/com/devcard/devcard/auth/controller/rest/LoginController.java similarity index 81% rename from src/main/java/com/devcard/devcard/auth/controller/LoginController.java rename to src/main/java/com/devcard/devcard/auth/controller/rest/LoginController.java index 1cdaecb..549d419 100644 --- a/src/main/java/com/devcard/devcard/auth/controller/LoginController.java +++ b/src/main/java/com/devcard/devcard/auth/controller/rest/LoginController.java @@ -1,4 +1,4 @@ -package com.devcard.devcard.auth.controller; +package com.devcard.devcard.auth.controller.rest; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; @@ -6,7 +6,7 @@ @Controller("gitHubLoginController") public class LoginController { - @GetMapping("login") + @GetMapping("/login") public String login() { return "login"; } diff --git a/src/main/java/com/devcard/devcard/auth/controller/OauthController.java b/src/main/java/com/devcard/devcard/auth/controller/rest/OauthController.java similarity index 86% rename from src/main/java/com/devcard/devcard/auth/controller/OauthController.java rename to src/main/java/com/devcard/devcard/auth/controller/rest/OauthController.java index 42f2077..043151e 100644 --- a/src/main/java/com/devcard/devcard/auth/controller/OauthController.java +++ b/src/main/java/com/devcard/devcard/auth/controller/rest/OauthController.java @@ -1,4 +1,4 @@ -package com.devcard.devcard.auth.controller; +package com.devcard.devcard.auth.controller.rest; import com.devcard.devcard.auth.entity.Member; import com.devcard.devcard.auth.model.OauthMemberDetails; @@ -6,10 +6,8 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; -import org.springframework.security.oauth2.core.user.OAuth2User; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.security.core.Authentication; import java.util.HashMap; import java.util.Map; diff --git a/src/main/java/com/devcard/devcard/auth/entity/Member.java b/src/main/java/com/devcard/devcard/auth/entity/Member.java index 7d856e4..1aaf439 100644 --- a/src/main/java/com/devcard/devcard/auth/entity/Member.java +++ b/src/main/java/com/devcard/devcard/auth/entity/Member.java @@ -21,7 +21,10 @@ public class Member { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String githubId; + + @Column(name = "email") private String email; + private String profileImg; private String username; private String nickname; @@ -30,8 +33,8 @@ public class Member { @CreationTimestamp private Timestamp createDate; - @OneToOne(mappedBy = "member", cascade = CascadeType.ALL) - private Card card; + @OneToMany(mappedBy = "member", cascade = CascadeType.ALL) + private List cards = new ArrayList<>(); @OneToMany(mappedBy = "member", cascade = CascadeType.ALL) private List groups = new ArrayList<>(); @@ -50,7 +53,7 @@ public Member(String githubId, String email, String profileImg, String username, } public void updateFromAttributes(Map attributes) { - this.email = (String) attributes.get("email"); + this.email = email; this.profileImg = (String) attributes.get("avatar_url"); this.username = (String) attributes.get("name"); this.nickname = (String) attributes.get("login"); diff --git a/src/main/java/com/devcard/devcard/auth/model/OauthMemberDetails.java b/src/main/java/com/devcard/devcard/auth/model/OauthMemberDetails.java index 8db2807..ec040b8 100644 --- a/src/main/java/com/devcard/devcard/auth/model/OauthMemberDetails.java +++ b/src/main/java/com/devcard/devcard/auth/model/OauthMemberDetails.java @@ -20,7 +20,7 @@ public OauthMemberDetails(Member member, Map attributes){ @Override public A getAttribute(String name) { - return OAuth2User.super.getAttribute(name); + return (A) attributes.get(name); } @Override diff --git a/src/main/java/com/devcard/devcard/auth/repository/MemberRepository.java b/src/main/java/com/devcard/devcard/auth/repository/MemberRepository.java index b4c269e..87d0d10 100644 --- a/src/main/java/com/devcard/devcard/auth/repository/MemberRepository.java +++ b/src/main/java/com/devcard/devcard/auth/repository/MemberRepository.java @@ -1,8 +1,12 @@ package com.devcard.devcard.auth.repository; import com.devcard.devcard.auth.entity.Member; +import java.util.List; import org.springframework.data.jpa.repository.JpaRepository; public interface MemberRepository extends JpaRepository { + Member findByGithubId(String githubId); + + List findByIdIn(List ids); } diff --git a/src/main/java/com/devcard/devcard/auth/service/OauthService.java b/src/main/java/com/devcard/devcard/auth/service/OauthService.java index 9a9d1e2..7dc1791 100644 --- a/src/main/java/com/devcard/devcard/auth/service/OauthService.java +++ b/src/main/java/com/devcard/devcard/auth/service/OauthService.java @@ -1,53 +1,102 @@ -package com.devcard.devcard.auth.service; - -import com.devcard.devcard.auth.entity.Member; -import com.devcard.devcard.auth.model.OauthMemberDetails; -import com.devcard.devcard.auth.repository.MemberRepository; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService; -import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest; -import org.springframework.security.oauth2.core.OAuth2AuthenticationException; -import org.springframework.security.oauth2.core.user.OAuth2User; -import org.springframework.stereotype.Service; - -import java.sql.Timestamp; -import java.util.Map; - -@Service -public class OauthService extends DefaultOAuth2UserService { - - @Autowired - private MemberRepository memberRepository; - - @Override - public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException{ - OAuth2User oAuth2User = super.loadUser(userRequest); - - Map attributes = oAuth2User.getAttributes(); - Integer githubIdInt = (Integer) attributes.get("id"); - String githubId = String.valueOf(githubIdInt); - - // 데이터베이스에 이미 등록된 사용자인지 확인 - Member member = memberRepository.findByGithubId(githubId); - if (member == null) { - // 사용자가 없으면 새로 저장 - member = new Member( - githubId, - (String) attributes.get("email"), - (String) attributes.get("avatar_url"), - (String) attributes.get("name"), - (String) attributes.get("login"), - "ROLE_USER", // 기본 역할 설정 - new Timestamp(System.currentTimeMillis()) - ); - memberRepository.save(member); - } else { - // 기존 회원 정보 업데이트 - member.updateFromAttributes(attributes); - memberRepository.save(member); - } - - // OauthMemberDetails 반환 - return new OauthMemberDetails(member, attributes); - } +package com.devcard.devcard.auth.service; + +import com.devcard.devcard.auth.entity.Member; +import com.devcard.devcard.auth.model.OauthMemberDetails; +import com.devcard.devcard.auth.repository.MemberRepository; +import com.devcard.devcard.card.service.CardService; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.ResponseEntity; +import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService; +import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.core.user.OAuth2User; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; + +import java.sql.Timestamp; +import java.util.List; +import java.util.Map; + +@Service +public class OauthService extends DefaultOAuth2UserService { + + private final MemberRepository memberRepository; + private final CardService cardService; + + public OauthService(MemberRepository memberRepository, CardService cardService) { + this.memberRepository = memberRepository; + this.cardService = cardService; + } + + @Override + public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException{ + OAuth2User oAuth2User = super.loadUser(userRequest); + + Map attributes = oAuth2User.getAttributes(); + Integer githubIdInt = (Integer) attributes.get("id"); + String githubId = String.valueOf(githubIdInt); + + String email = fetchEmailFromGitHub(userRequest); + + // 데이터베이스에 이미 등록된 사용자인지 확인 + Member member = memberRepository.findByGithubId(githubId); + if (member == null) { + // 사용자가 없으면 새로 저장 + member = new Member( + githubId, + email, + (String) attributes.get("avatar_url"), + (String) attributes.get("name"), + (String) attributes.get("login"), + "ROLE_USER", // 기본 역할 설정 + new Timestamp(System.currentTimeMillis()) + ); + memberRepository.save(member); + + // 회원가입시 멤버 정보로 명함 자동 생성 + cardService.createCardWithDefaultInfo(member); + } else { + // 기존 회원 정보 업데이트 + member.updateFromAttributes(attributes); + memberRepository.save(member); + } + + // OauthMemberDetails 반환 + return new OauthMemberDetails(member, attributes); + } + + // 이메일 정보를 가져오는 메서드 추가 + private String fetchEmailFromGitHub(OAuth2UserRequest userRequest) { + String uri = "https://api.github.com/user/emails"; + String accessToken = userRequest.getAccessToken().getTokenValue(); + + HttpHeaders headers = new HttpHeaders(); + headers.add("Authorization", "token " + accessToken); + HttpEntity entity = new HttpEntity<>(headers); + + RestTemplate restTemplate = new RestTemplate(); + ResponseEntity>> response = restTemplate.exchange( + uri, + HttpMethod.GET, + entity, + new ParameterizedTypeReference>>() {} + ); + + List> emails = response.getBody(); + + if (emails != null) { + for (Map emailInfo : emails) { + Boolean primary = (Boolean) emailInfo.get("primary"); + Boolean verified = (Boolean) emailInfo.get("verified"); + if (primary != null && primary && verified != null && verified) { + return (String) emailInfo.get("email"); + } + } + } + + return null; + } } \ No newline at end of file diff --git a/src/main/java/com/devcard/devcard/card/controller/page/CardPageController.java b/src/main/java/com/devcard/devcard/card/controller/page/CardPageController.java index 14e1d72..3bb95f6 100644 --- a/src/main/java/com/devcard/devcard/card/controller/page/CardPageController.java +++ b/src/main/java/com/devcard/devcard/card/controller/page/CardPageController.java @@ -2,7 +2,10 @@ import com.devcard.devcard.auth.model.OauthMemberDetails; import com.devcard.devcard.card.dto.CardResponseDto; +import com.devcard.devcard.card.dto.GroupResponseDto; +import com.devcard.devcard.card.repository.GroupRepository; import com.devcard.devcard.card.service.CardService; +import com.devcard.devcard.card.service.GroupService; import org.springframework.beans.factory.annotation.Value; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.stereotype.Controller; @@ -16,19 +19,31 @@ public class CardPageController { private final CardService cardService; + private final GroupService groupService; + private final GroupRepository groupRepository; @Value("${kakao.javascript.key}") private String kakaoJavascriptKey; - public CardPageController(CardService cardService) { + public CardPageController(CardService cardService, GroupService groupService, GroupRepository groupRepository) { this.cardService = cardService; + this.groupService = groupService; + this.groupRepository = groupRepository; } @GetMapping("/cards/{id}/view") - public String viewCard(@PathVariable("id") Long id, Model model) { + public String viewCard(@PathVariable("id") Long id, Model model, @AuthenticationPrincipal OauthMemberDetails oauthMemberDetails) { CardResponseDto card = cardService.getCard(id); + boolean isMyCard = card.getGithubId().equals(oauthMemberDetails.getMember().getGithubId()); model.addAttribute("card", card); + model.addAttribute("isMyCard", isMyCard); model.addAttribute("kakaoJavascriptKey", kakaoJavascriptKey); + + if (!isMyCard) { + List groups = groupService.getGroupsByMember(oauthMemberDetails.getMember()); + model.addAttribute("groups", groups); + } + return "card-detail"; } @@ -43,7 +58,31 @@ public String updateCard(@PathVariable("id") Long id, Model model) { public String viewCardList(@PathVariable("id") Long groupId, Model model, @AuthenticationPrincipal OauthMemberDetails oauthMemberDetails) { List cards = cardService.getCardsByGroup(groupId, oauthMemberDetails.getMember()); model.addAttribute("cards", cards); + model.addAttribute("group", groupRepository.findById(groupId).get()); return "card-list"; } + @GetMapping("/cards/manage") + public String cardManage() { + return "card-manage"; + } + + @GetMapping("/cards/create-view") + public String createCardView() { + return "card-create"; + } + + @GetMapping("/cards/{cardId}/edit") + public String editCardView(@PathVariable("cardId") Long cardId, Model model) { + model.addAttribute("cardId", cardId); + return "card-edit"; + } + + @GetMapping("/shared/cards/{cardId}") + public String sharedCardView(@PathVariable("cardId") Long cardId, Model model) { + CardResponseDto card = cardService.getCard(cardId); + model.addAttribute("card", card); + return "card-shared-view"; + } + } diff --git a/src/main/java/com/devcard/devcard/card/controller/page/HomeController.java b/src/main/java/com/devcard/devcard/card/controller/page/HomeController.java index cc0b0f8..d711013 100644 --- a/src/main/java/com/devcard/devcard/card/controller/page/HomeController.java +++ b/src/main/java/com/devcard/devcard/card/controller/page/HomeController.java @@ -1,14 +1,43 @@ package com.devcard.devcard.card.controller.page; +import com.devcard.devcard.auth.entity.Member; +import com.devcard.devcard.auth.model.OauthMemberDetails; +import com.devcard.devcard.card.dto.CardResponseDto; +import com.devcard.devcard.card.service.CardService; +import java.util.List; +import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; @Controller public class HomeController { + private final CardService cardService; + + public HomeController(CardService cardService) { + this.cardService = cardService; + } + @GetMapping("/home") - public String home() { + public String home(@AuthenticationPrincipal OauthMemberDetails oauthMemberDetails, Model model) { + if (oauthMemberDetails != null) { + Member member = oauthMemberDetails.getMember(); + model.addAttribute("member", member); + + // CardResponseDto 리스트 가져오기 + List cards = cardService.getMyCards(member.getId()); + + // 첫 번째 카드 ID 가져오기 (카드가 존재하는 경우) + if (!cards.isEmpty()) { + Long firstCardId = cards.getFirst().getId(); + model.addAttribute("cardId", firstCardId); + } else { + // 기본 카드 ID 또는 처리 방법 정의 (카드가 없을 경우) + model.addAttribute("cardId", 1); // 기본값으로 1 설정 + } + } return "home"; } } diff --git a/src/main/java/com/devcard/devcard/card/controller/rest/CardController.java b/src/main/java/com/devcard/devcard/card/controller/rest/CardController.java index 1377b37..381293a 100644 --- a/src/main/java/com/devcard/devcard/card/controller/rest/CardController.java +++ b/src/main/java/com/devcard/devcard/card/controller/rest/CardController.java @@ -4,6 +4,7 @@ import com.devcard.devcard.auth.model.OauthMemberDetails; import com.devcard.devcard.card.dto.CardResponseDto; import com.devcard.devcard.card.dto.CardRequestDto; +import com.devcard.devcard.card.dto.CardUpdateDto; import com.devcard.devcard.card.service.CardService; import jakarta.validation.Valid; import org.springframework.http.ResponseEntity; @@ -38,13 +39,20 @@ public ResponseEntity createCard( return ResponseEntity.created(location).body(responseDto); } - @GetMapping("/{id}") public ResponseEntity getCard(@PathVariable Long id) { CardResponseDto responseDto = cardService.getCard(id); return ResponseEntity.ok(responseDto); } + @GetMapping("/my") + public ResponseEntity> getMyCards( + @AuthenticationPrincipal OauthMemberDetails oauthMemberDetails) { + Member member = oauthMemberDetails.getMember(); + List responseDto = cardService.getMyCards(member.getId()); + return ResponseEntity.ok(responseDto); + } + @GetMapping() public ResponseEntity> getCards() { List responseDto = cardService.getCards(); @@ -54,11 +62,11 @@ public ResponseEntity> getCards() { @PutMapping("/{id}") public ResponseEntity updateCard( @PathVariable Long id, - @Valid @RequestBody CardRequestDto cardRequestDto, + @Valid @RequestBody CardUpdateDto cardUpdateDto, @AuthenticationPrincipal OauthMemberDetails oauthMemberDetails) { Member member = oauthMemberDetails.getMember(); - CardResponseDto responseDto = cardService.updateCard(id, cardRequestDto, member); + CardResponseDto responseDto = cardService.updateCard(id, cardUpdateDto, member); return ResponseEntity.ok(responseDto); } @@ -72,14 +80,4 @@ public ResponseEntity deleteCard( return ResponseEntity.noContent().build(); } - @PostMapping("/{id}/add-to-group/{groupId}") - public ResponseEntity addCardToGroup( - @PathVariable Long id, - @PathVariable Long groupId - ) { - cardService.addCardToGroup(id, groupId); - - return ResponseEntity.ok().build(); - } - } diff --git a/src/main/java/com/devcard/devcard/card/controller/rest/GroupController.java b/src/main/java/com/devcard/devcard/card/controller/rest/GroupController.java index 9819e11..1c97860 100644 --- a/src/main/java/com/devcard/devcard/card/controller/rest/GroupController.java +++ b/src/main/java/com/devcard/devcard/card/controller/rest/GroupController.java @@ -1,14 +1,14 @@ package com.devcard.devcard.card.controller.rest; +import com.devcard.devcard.auth.entity.Member; import com.devcard.devcard.auth.model.OauthMemberDetails; import com.devcard.devcard.card.dto.GroupResponseDto; import com.devcard.devcard.card.service.GroupService; import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; + +import java.util.Map; @RestController @RequestMapping("/groups") @@ -26,4 +26,34 @@ public ResponseEntity createGroup(@AuthenticationPrincipal Oau return ResponseEntity.ok(group); } + @PostMapping("/{groupId}/cards/{cardId}") + public ResponseEntity addCardToGroup(@PathVariable Long groupId, @PathVariable Long cardId, @AuthenticationPrincipal OauthMemberDetails oauthMemberDetails) { + groupService.addCardToGroup(groupId, cardId, oauthMemberDetails.getMember()); + return ResponseEntity.ok().build(); + } + + @PostMapping("/{groupId}/update") + public ResponseEntity updateGroupName(@PathVariable Long groupId, + @RequestBody Map request, + @AuthenticationPrincipal OauthMemberDetails oauthMemberDetails) { + String newName = request.get("name"); + Member member = oauthMemberDetails.getMember(); // OauthMemberDetails에서 Member 객체 가져오기 + groupService.updateGroupName(groupId, newName, member); + return ResponseEntity.ok("그룹 이름이 수정되었습니다."); + } + + @DeleteMapping("/{groupId}/cards/{cardId}/delete") + public ResponseEntity removeCardFromGroup(@PathVariable Long groupId, @PathVariable Long cardId, @AuthenticationPrincipal OauthMemberDetails oauthMemberDetails) { + groupService.deleteCardFromGroup(groupId, cardId, oauthMemberDetails.getMember()); + return ResponseEntity.ok().build(); + } + + @DeleteMapping("/{groupId}/delete") + public ResponseEntity deleteGroup(@PathVariable Long groupId, @AuthenticationPrincipal OauthMemberDetails oauthMemberDetails) { + groupService.deleteGroup(groupId, oauthMemberDetails.getMember()); + return ResponseEntity.ok().build(); + } + + + } diff --git a/src/main/java/com/devcard/devcard/card/controller/rest/QrController.java b/src/main/java/com/devcard/devcard/card/controller/rest/QrController.java index a73e2a2..39b7466 100644 --- a/src/main/java/com/devcard/devcard/card/controller/rest/QrController.java +++ b/src/main/java/com/devcard/devcard/card/controller/rest/QrController.java @@ -1,31 +1,35 @@ -package com.devcard.devcard.card.controller.rest; - -import com.devcard.devcard.card.dto.QrResponseDto; -import com.devcard.devcard.card.service.QrServiceImpl; -import com.google.zxing.WriterException; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.RestController; - -import java.io.IOException; - -@RestController -public class QrController { - - private final QrServiceImpl qrServiceImpl; - - @Autowired - public QrController(QrServiceImpl qrServiceImpl) { - this.qrServiceImpl = qrServiceImpl; - } - - @GetMapping("/cards/{card_id}/qrcode") - public ResponseEntity createQR(@PathVariable (name = "card_id") Long cardId) throws IOException, WriterException { - String qrUrl = qrServiceImpl.createQr(cardId); - - return ResponseEntity.ok() - .body(new QrResponseDto(qrUrl)); - } -} +package com.devcard.devcard.card.controller.rest; + +import com.devcard.devcard.card.service.QrServiceImpl; +import com.google.zxing.WriterException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RestController; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +@RestController +public class QrController { + + private final QrServiceImpl qrServiceImpl; + + @Autowired + public QrController(QrServiceImpl qrServiceImpl) { + this.qrServiceImpl = qrServiceImpl; + } + + @GetMapping("/cards/{card_id}/qrcode-image") + public ResponseEntity generateQrImage(@PathVariable(name = "card_id") Long cardId) throws IOException, WriterException { + ByteArrayOutputStream qrImageStream = qrServiceImpl.generateQrImageStream(cardId); + + return ResponseEntity.ok() + .header(HttpHeaders.CONTENT_DISPOSITION, "inline; filename=\"qrcode.png\"") + .contentType(MediaType.IMAGE_PNG) + .body(qrImageStream.toByteArray()); + } +} diff --git a/src/main/java/com/devcard/devcard/card/dto/CardRequestDto.java b/src/main/java/com/devcard/devcard/card/dto/CardRequestDto.java index aab0e45..abe59ff 100644 --- a/src/main/java/com/devcard/devcard/card/dto/CardRequestDto.java +++ b/src/main/java/com/devcard/devcard/card/dto/CardRequestDto.java @@ -12,19 +12,27 @@ public class CardRequestDto { @Pattern(regexp = "^[+]?[0-9\\-\\s]+$", message = "유효한 전화번호 형식을 입력하세요.") private final String phone; - private final String bio; + private final String email; + private final String cardName; + private final String profileImg; - public CardRequestDto(String company, String position, String phone, String bio) { + public CardRequestDto(String company, String position, String phone, String bio, String email, String cardName, String profileImg) { this.company = company; this.position = position; this.phone = phone; this.bio = bio; + this.email = email; + this.cardName = cardName; + this.profileImg = profileImg; } public String getCompany() { return company; } public String getPosition() { return position; } public String getPhone() { return phone; } public String getBio() { return bio; } + public String getEmail() { return email; } + public String getCardName() { return cardName; } + public String getProfileImg() { return profileImg; } } diff --git a/src/main/java/com/devcard/devcard/card/dto/CardResponseDto.java b/src/main/java/com/devcard/devcard/card/dto/CardResponseDto.java index 61c52e8..5328e26 100644 --- a/src/main/java/com/devcard/devcard/card/dto/CardResponseDto.java +++ b/src/main/java/com/devcard/devcard/card/dto/CardResponseDto.java @@ -1,66 +1,102 @@ -package com.devcard.devcard.card.dto; - -import com.devcard.devcard.auth.entity.Member; -import com.devcard.devcard.card.entity.Card; - -import java.time.LocalDateTime; - -public class CardResponseDto { - - private final Long id; - private final String name; - private final String company; - private final String position; - private final String email; - private final String phone; - private final String githubId; - private final String bio; - private final String profilePicture; - private final LocalDateTime createdAt; - private final LocalDateTime updatedAt; - - public CardResponseDto(Long id, String name, String company, String position, String email, String phone, - String githubId, String bio, String profilePicture, - LocalDateTime createdAt, LocalDateTime updatedAt) { - this.id = id; - this.name = name; - this.company = company; - this.position = position; - this.email = email; - this.phone = phone; - this.githubId = githubId; - this.bio = bio; - this.profilePicture = profilePicture; - this.createdAt = createdAt; - this.updatedAt = updatedAt; - } - - public Long getId() { return id; } - public String getName() { return name; } - public String getCompany() { return company; } - public String getPosition() { return position; } - public String getEmail() { return email; } - public String getPhone() { return phone; } - public String getGithubId() { return githubId; } - public String getBio() { return bio; } - public String getProfilePicture() { return profilePicture; } - public LocalDateTime getCreatedAt() { return createdAt; } - public LocalDateTime getUpdatedAt() { return updatedAt; } - - public static CardResponseDto fromEntity(Card card) { - Member member = card.getMember(); - return new CardResponseDto( - card.getId(), - member.getUsername(), - card.getCompany(), - card.getPosition(), - member.getEmail(), - card.getPhone(), - member.getGithubId(), - card.getBio(), - member.getProfileImg(), - card.getCreatedAt(), - card.getUpdatedAt() - ); - } -} +package com.devcard.devcard.card.dto; + +import com.devcard.devcard.auth.entity.Member; +import com.devcard.devcard.card.entity.Card; +import com.fasterxml.jackson.annotation.JsonInclude; + +import java.time.LocalDateTime; + +@JsonInclude(JsonInclude.Include.NON_NULL) +public class CardResponseDto { + + private final Long id; + private final String name; + private final String nickname; + private final String company; + private final String position; + private final String email; + private final String phone; + private final String githubId; + private final String bio; + private final String profileImg; + private final String linkedin; + private final String notion; + private final String certification; + private final String extra; + private final boolean techStack; + private final boolean repository; + private final boolean contributions; + private final LocalDateTime createdAt; + private final LocalDateTime updatedAt; + + public CardResponseDto(Long id, String name, String nickname, String company, String position, String email, + String phone, String githubId, String bio, String profileImg, + String linkedin, String notion, String certification, String extra, + boolean techStack, boolean repository, boolean contributions, + LocalDateTime createdAt, LocalDateTime updatedAt) { + this.id = id; + this.name = name; + this.nickname = nickname; + this.company = company; + this.position = position; + this.email = email; + this.phone = phone; + this.githubId = githubId; + this.bio = bio; + this.profileImg = profileImg; + this.linkedin = linkedin; + this.notion = notion; + this.certification = certification; + this.extra = extra; + this.techStack = techStack; + this.repository = repository; + this.contributions = contributions; + this.createdAt = createdAt; + this.updatedAt = updatedAt; + } + + public Long getId() { return id; } + public String getName() { return name; } + public String getNickname() { return nickname; } + public String getCompany() { return company; } + public String getPosition() { return position; } + public String getEmail() { return email; } + public String getPhone() { return phone; } + public String getGithubId() { return githubId; } + public String getBio() { return bio; } + public String getProfileImg() { return profileImg; } + public String getLinkedin() { return linkedin; } + public String getNotion() { return notion; } + public String getCertification() { return certification; } + public String getExtra() { return extra; } + public boolean isTechStack() { return techStack; } + public boolean isRepository() { return repository; } + public boolean isContributions() { return contributions; } + public LocalDateTime getCreatedAt() { return createdAt; } + public LocalDateTime getUpdatedAt() { return updatedAt; } + + public static CardResponseDto fromEntity(Card card) { + Member member = card.getMember(); + return new CardResponseDto( + card.getId(), + member.getUsername(), + card.getNickname(), + card.getCompany(), + card.getPosition(), + member.getEmail(), + card.getPhone(), + member.getGithubId(), + card.getBio(), + member.getProfileImg(), + card.getLinkedin(), + card.getNotion(), + card.getCertification(), + card.getExtra(), + card.isTechStack(), + card.isRepository(), + card.isContributions(), + card.getCreatedAt(), + card.getUpdatedAt() + ); + } +} diff --git a/src/main/java/com/devcard/devcard/card/dto/CardUpdateDto.java b/src/main/java/com/devcard/devcard/card/dto/CardUpdateDto.java new file mode 100644 index 0000000..50aac85 --- /dev/null +++ b/src/main/java/com/devcard/devcard/card/dto/CardUpdateDto.java @@ -0,0 +1,53 @@ +package com.devcard.devcard.card.dto; + +public class CardUpdateDto { + + private final String company; + private final String position; + private final String phone; + private final String bio; + private final String email; + private final String cardName; + private final String profileImg; + private final String linkedin; + private final String notion; + private final String certification; + private final String extra; + private final boolean techStack; + private final boolean repository; + private final boolean contributions; + + public CardUpdateDto(String company, String position, String phone, String bio, String email, String cardName, String profileImg, + String linkedin, String notion, String certification, String extra, + boolean techStack, boolean repository, boolean contributions) { + this.company = company; + this.position = position; + this.phone = phone; + this.bio = bio; + this.email = email; + this.cardName = cardName; + this.profileImg = profileImg; + this.linkedin = linkedin; + this.notion = notion; + this.certification = certification; + this.extra = extra; + this.techStack = techStack; + this.repository = repository; + this.contributions = contributions; + } + + public String getCompany() { return company; } + public String getPosition() { return position; } + public String getPhone() { return phone; } + public String getBio() { return bio; } + public String getEmail() { return email; } + public String getCardName() { return cardName; } + public String getProfileImg() { return profileImg; } + public String getLinkedin() { return linkedin; } + public String getNotion() { return notion; } + public String getCertification() { return certification; } + public String getExtra() { return extra; } + public boolean isTechStack() { return techStack; } + public boolean isRepository() { return repository; } + public boolean isContributions() { return contributions; } +} diff --git a/src/main/java/com/devcard/devcard/card/dto/QrResponseDto.java b/src/main/java/com/devcard/devcard/card/dto/QrResponseDto.java deleted file mode 100644 index 7da8c17..0000000 --- a/src/main/java/com/devcard/devcard/card/dto/QrResponseDto.java +++ /dev/null @@ -1,6 +0,0 @@ -package com.devcard.devcard.card.dto; - -public record QrResponseDto( - String qrcode_url -) { -} diff --git a/src/main/java/com/devcard/devcard/card/entity/Card.java b/src/main/java/com/devcard/devcard/card/entity/Card.java index 4b3736f..cc19645 100644 --- a/src/main/java/com/devcard/devcard/card/entity/Card.java +++ b/src/main/java/com/devcard/devcard/card/entity/Card.java @@ -1,106 +1,209 @@ -package com.devcard.devcard.card.entity; - -import com.devcard.devcard.auth.entity.Member; -import com.devcard.devcard.card.dto.CardRequestDto; -import jakarta.persistence.*; -import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.List; - -@Entity -@Table(name = "card") -public class Card { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @OneToOne - @JoinColumn(name = "member_id") - private Member member; - - @ManyToMany(mappedBy = "cards") - private List groups = new ArrayList<>(); - - private String company; - private String position; - private String phone; - private String bio; - private LocalDateTime createdAt; - private LocalDateTime updatedAt; - - // 기본 생성자 (JPA 요구 사항) - protected Card() { - } - - // 빌더 패턴을 위한 생성자 - private Card(Builder builder) { - this.member = builder.member; - this.company = builder.company; - this.position = builder.position; - this.phone = builder.phone; - this.bio = builder.bio; - this.createdAt = LocalDateTime.now(); - this.updatedAt = LocalDateTime.now(); - } - - public static class Builder { - private final Member member; - private String company; - private String position; - private String phone; - private String bio; - - public Builder(Member member) { - this.member = member; - } - - public Builder company(String company) { - this.company = company; - return this; - } - - public Builder position(String position) { - this.position = position; - return this; - } - - public Builder phone(String phone) { - this.phone = phone; - return this; - } - - public Builder bio(String bio) { - this.bio = bio; - return this; - } - - public Card build() { - return new Card(this); - } - } - - // Getter - public Long getId() { return id; } - public Member getMember() { return member; } - public String getCompany() { return company; } - public String getPosition() { return position; } - public String getPhone() { return phone; } - public String getBio() { return bio; } - public LocalDateTime getCreatedAt() { return createdAt; } - public LocalDateTime getUpdatedAt() { return updatedAt; } - public List getGroups() { return groups; } - - public void setGroups(List groups) { - this.groups = groups; - } - - // DTO 기반 업데이트 메서드 - public void updateFromDto(CardRequestDto dto) { - if (dto.getCompany() != null) this.company = dto.getCompany(); - if (dto.getPosition() != null) this.position = dto.getPosition(); - if (dto.getPhone() != null) this.phone = dto.getPhone(); - if (dto.getBio() != null) this.bio = dto.getBio(); - this.updatedAt = LocalDateTime.now(); - } -} +package com.devcard.devcard.card.entity; + +import com.devcard.devcard.auth.entity.Member; +import com.devcard.devcard.card.dto.CardUpdateDto; +import jakarta.persistence.*; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +@Entity +@Table(name = "card") +public class Card { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne + @JoinColumn(name = "member_id") + private Member member; + + @ManyToMany(mappedBy = "cards") + private List groups = new ArrayList<>(); + + private String email; + private String nickname; + private String profileImg; + private String company; + private String position; + private String phone; + private String bio; + private String linkedin; + private String notion; + private String certification; + private String extra; + + private boolean techStack; + private boolean repository; + private boolean contributions; + + private LocalDateTime createdAt; + private LocalDateTime updatedAt; + + // 기본 생성자 (JPA 요구 사항) + protected Card() { + } + + // 빌더 패턴을 위한 생성자 + private Card(Builder builder) { + this.member = builder.member; + this.email = builder.email; + this.nickname = builder.nickname; + this.profileImg = builder.profileImg; + this.company = builder.company; + this.position = builder.position; + this.phone = builder.phone; + this.bio = builder.bio; + this.linkedin = builder.linkedin; + this.notion = builder.notion; + this.certification = builder.certification; + this.extra = builder.extra; + this.techStack = builder.techStack; + this.repository = builder.repository; + this.contributions = builder.contributions; + this.createdAt = LocalDateTime.now(); + this.updatedAt = LocalDateTime.now(); + } + + // 빌더 패턴을 위한 생성자 + public static class Builder { + private final Member member; + private String email; + private String nickname; + private String profileImg; + private String company; + private String position; + private String phone; + private String bio; + private String linkedin; + private String notion; + private String certification; + private String extra; + private boolean techStack; + private boolean repository; + private boolean contributions; + + public Builder(Member member) { + this.member = member; + } + + public Builder email(String email) { + this.email = email; + return this; + } + + public Builder nickname(String nickname) { + this.nickname = nickname; + return this; + } + + public Builder profileImg(String profileImg) { + this.profileImg = profileImg; + return this; + } + + public Builder company(String company) { + this.company = company; + return this; + } + + public Builder position(String position) { + this.position = position; + return this; + } + + public Builder phone(String phone) { + this.phone = phone; + return this; + } + + public Builder bio(String bio) { + this.bio = bio; + return this; + } + + public Builder linkedin(String linkedin) { + this.linkedin = linkedin; + return this; + } + + public Builder notion(String notion) { + this.notion = notion; + return this; + } + + public Builder certification(String certification) { + this.certification = certification; + return this; + } + + public Builder extra(String extra) { + this.extra = extra; + return this; + } + + public Builder techStack(boolean techStack) { + this.techStack = techStack; + return this; + } + + public Builder repository(boolean repository) { + this.repository = repository; + return this; + } + + public Builder contributions(boolean contributions) { + this.contributions = contributions; + return this; + } + + public Card build() { + return new Card(this); + } + } + + // Getter + public Long getId() { return id; } + public Member getMember() { return member; } + public String getEmail() { return email; } + public String getNickname() { return nickname; } + public String getProfileImg() { return profileImg; } + public String getCompany() { return company; } + public String getPosition() { return position; } + public String getPhone() { return phone; } + public String getBio() { return bio; } + public String getLinkedin() { return linkedin; } + public String getNotion() { return notion; } + public String getCertification() { return certification; } + public String getExtra() { return extra; } + public boolean isTechStack() { return techStack; } + public boolean isRepository() { return repository; } + public boolean isContributions() { return contributions; } + public LocalDateTime getCreatedAt() { return createdAt; } + public LocalDateTime getUpdatedAt() { return updatedAt; } + public List getGroups() { return groups; } + + public void setGroups(List groups) { + this.groups = groups; + } + + // DTO 기반 업데이트 메서드 + public void updateFromDto(CardUpdateDto dto) { + this.company = dto.getCompany(); + this.position = dto.getPosition(); + this.phone = dto.getPhone(); + this.bio = dto.getBio(); + this.email = dto.getEmail(); + this.nickname = dto.getCardName(); + this.profileImg = dto.getProfileImg(); + this.linkedin = dto.getLinkedin(); + this.notion = dto.getNotion(); + this.certification = dto.getCertification(); + this.extra = dto.getExtra(); + this.techStack = dto.isTechStack(); + this.repository = dto.isRepository(); + this.contributions = dto.isContributions(); + this.updatedAt = LocalDateTime.now(); + } +} diff --git a/src/main/java/com/devcard/devcard/card/entity/Group.java b/src/main/java/com/devcard/devcard/card/entity/Group.java index b837cff..0d3504b 100644 --- a/src/main/java/com/devcard/devcard/card/entity/Group.java +++ b/src/main/java/com/devcard/devcard/card/entity/Group.java @@ -7,7 +7,7 @@ import java.util.List; @Entity -@Table(name = "groups") +@Table(name = "`groups`") public class Group { @Id @@ -62,4 +62,8 @@ public void removeCard(Card card) { public int getCount() { return cards.size(); } + + public void setName(String name) { + this.name = name; + } } diff --git a/src/main/java/com/devcard/devcard/card/repository/CardRepository.java b/src/main/java/com/devcard/devcard/card/repository/CardRepository.java index 9000436..9db324a 100644 --- a/src/main/java/com/devcard/devcard/card/repository/CardRepository.java +++ b/src/main/java/com/devcard/devcard/card/repository/CardRepository.java @@ -3,5 +3,8 @@ import com.devcard.devcard.card.entity.Card; import org.springframework.data.jpa.repository.JpaRepository; +import java.util.List; + public interface CardRepository extends JpaRepository{ + List findByMemberId(Long memberId); } diff --git a/src/main/java/com/devcard/devcard/card/repository/GroupRepository.java b/src/main/java/com/devcard/devcard/card/repository/GroupRepository.java index a34e1dd..3a5740e 100644 --- a/src/main/java/com/devcard/devcard/card/repository/GroupRepository.java +++ b/src/main/java/com/devcard/devcard/card/repository/GroupRepository.java @@ -2,12 +2,23 @@ import com.devcard.devcard.auth.entity.Member; import com.devcard.devcard.card.entity.Group; -import org.springframework.data.jpa.repository.JpaRepository; - import java.util.List; import java.util.Optional; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +public interface GroupRepository extends JpaRepository { -public interface GroupRepository extends JpaRepository{ List findByMember(Member member); + Optional findByIdAndMember(Long id, Member member); + + @Query( + "SELECT CASE WHEN COUNT(g) > 0 THEN true ELSE false END " + + "FROM Group g " + + "JOIN g.cards c " + + "WHERE g.member = :member AND c.member.id = :cardOwnerId" + ) + boolean existsByMemberAndCards_Id(@Param("member") Member member, @Param("cardOwnerId") Long cardOwnerId); } diff --git a/src/main/java/com/devcard/devcard/card/service/CardService.java b/src/main/java/com/devcard/devcard/card/service/CardService.java index 28347b4..5a2dfb9 100644 --- a/src/main/java/com/devcard/devcard/card/service/CardService.java +++ b/src/main/java/com/devcard/devcard/card/service/CardService.java @@ -1,107 +1,130 @@ -package com.devcard.devcard.card.service; - -import com.devcard.devcard.auth.entity.Member; -import com.devcard.devcard.card.dto.CardRequestDto; -import com.devcard.devcard.card.dto.CardResponseDto; -import com.devcard.devcard.card.entity.Group; -import com.devcard.devcard.card.exception.CardNotFoundException; -import com.devcard.devcard.card.repository.CardRepository; -import com.devcard.devcard.card.entity.Card; -import com.devcard.devcard.card.repository.GroupRepository; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.util.List; -import java.util.stream.Collectors; - -@Service -public class CardService { - - private final CardRepository cardRepository; - private final GroupRepository groupRepository; - - public CardService(CardRepository cardRepository, GroupRepository groupRepository) { - this.cardRepository = cardRepository; - this.groupRepository = groupRepository; - } - - @Transactional - public CardResponseDto createCard(CardRequestDto cardRequestDto, Member member) { - Card card = new Card.Builder(member) - .company(cardRequestDto.getCompany()) - .position(cardRequestDto.getPosition()) - .phone(cardRequestDto.getPhone()) - .bio(cardRequestDto.getBio()) - .build(); - - Card savedCard = cardRepository.save(card); - return CardResponseDto.fromEntity(savedCard); - } - - @Transactional(readOnly = true) - public CardResponseDto getCard(Long cardId) { - Card card = cardRepository.findById(cardId) - .orElseThrow(() -> new CardNotFoundException("해당 명함을 찾을 수 없습니다.")); - return CardResponseDto.fromEntity(card); - } - - @Transactional(readOnly = true) - public List getCards() { - List cards = cardRepository.findAll(); - return cards.stream() - .map(CardResponseDto::fromEntity) - .toList(); - } - - @Transactional - public CardResponseDto updateCard(Long id, CardRequestDto cardRequestDto, Member member) { - Card existingCard = cardRepository.findById(id) - .orElseThrow(() -> new CardNotFoundException("해당 명함을 찾을 수 없습니다.")); - - // 명함 소유자 확인 - if (!existingCard.getMember().getId().equals(member.getId())) { - throw new RuntimeException("권한이 없습니다."); - } - - // 기존 엔티티의 필드 업데이트 - existingCard.updateFromDto(cardRequestDto); - - return CardResponseDto.fromEntity(existingCard); - } - - @Transactional - public void deleteCard(Long id, Member member) { - Card card = cardRepository.findById(id) - .orElseThrow(() -> new CardNotFoundException("해당 명함을 찾을 수 없습니다.")); - - // 명함 소유자 확인 - if (!card.getMember().getId().equals(member.getId())) { - throw new RuntimeException("권한이 없습니다."); - } - - cardRepository.delete(card); - } - - @Transactional - public void addCardToGroup(Long cardId, Long groupId) { - Group group = groupRepository.findById(groupId) - .orElseThrow(() -> new RuntimeException("해당 그룹을 찾을 수 없습니다.")); - - Card card = cardRepository.findById(cardId) - .orElseThrow(() -> new CardNotFoundException("해당 명함을 찾을 수 없습니다.")); - - group.addCard(card); - groupRepository.save(group); - } - - @Transactional(readOnly = true) - public List getCardsByGroup(Long groupId, Member member) { - Group group = groupRepository.findByIdAndMember(groupId, member) - .orElseThrow(() -> new RuntimeException("해당 그룹을 찾을 수 없습니다.")); - - return group.getCards().stream() - .map(CardResponseDto::fromEntity) - .collect(Collectors.toList()); - } - -} +package com.devcard.devcard.card.service; + +import com.devcard.devcard.auth.entity.Member; +import com.devcard.devcard.card.dto.CardRequestDto; +import com.devcard.devcard.card.dto.CardResponseDto; +import com.devcard.devcard.card.dto.CardUpdateDto; +import com.devcard.devcard.card.entity.Group; +import com.devcard.devcard.card.exception.CardNotFoundException; +import com.devcard.devcard.card.repository.CardRepository; +import com.devcard.devcard.card.entity.Card; +import com.devcard.devcard.card.repository.GroupRepository; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.stream.Collectors; + +@Service +public class CardService { + + private final CardRepository cardRepository; + private final GroupRepository groupRepository; + + public CardService(CardRepository cardRepository, GroupRepository groupRepository) { + this.cardRepository = cardRepository; + this.groupRepository = groupRepository; + } + + @Transactional + public void createCardWithDefaultInfo(Member member) { + Card card = new Card.Builder(member) + .email(member.getEmail()) + .nickname(member.getNickname()) + .profileImg(member.getProfileImg()) + .build(); + cardRepository.save(card); + } + + + @Transactional + public CardResponseDto createCard(CardRequestDto cardRequestDto, Member member) { + Card card = new Card.Builder(member) + .company(cardRequestDto.getCompany()) + .position(cardRequestDto.getPosition()) + .phone(cardRequestDto.getPhone()) + .bio(cardRequestDto.getBio()) + .email(cardRequestDto.getEmail()) + .nickname(cardRequestDto.getCardName()) + .profileImg(cardRequestDto.getProfileImg()) + .build(); + + Card savedCard = cardRepository.save(card); + return CardResponseDto.fromEntity(savedCard); + } + + @Transactional(readOnly = true) + public CardResponseDto getCard(Long cardId) { + Card card = cardRepository.findById(cardId) + .orElseThrow(() -> new CardNotFoundException("해당 명함을 찾을 수 없습니다.")); + return CardResponseDto.fromEntity(card); + } + + @Transactional(readOnly = true) + public List getMyCards(Long memberId) { + List cards = cardRepository.findByMemberId(memberId); + return cards.stream() + .map(CardResponseDto::fromEntity) + .toList(); + } + + @Transactional(readOnly = true) + public List getCards() { + List cards = cardRepository.findAll(); + return cards.stream() + .map(CardResponseDto::fromEntity) + .toList(); + } + + @Transactional + public CardResponseDto updateCard(Long id, CardUpdateDto cardUpdateDto, Member member) { + Card existingCard = cardRepository.findById(id) + .orElseThrow(() -> new CardNotFoundException("해당 명함을 찾을 수 없습니다.")); + + // 명함 소유자 확인 + if (!existingCard.getMember().getId().equals(member.getId())) { + throw new RuntimeException("권한이 없습니다."); + } + + // 기존 엔티티의 필드 업데이트 + existingCard.updateFromDto(cardUpdateDto); + + return CardResponseDto.fromEntity(existingCard); + } + + @Transactional + public void deleteCard(Long id, Member member) { + Card card = cardRepository.findById(id) + .orElseThrow(() -> new CardNotFoundException("해당 명함을 찾을 수 없습니다.")); + + // 명함 소유자 확인 + if (!card.getMember().getId().equals(member.getId())) { + throw new RuntimeException("권한이 없습니다."); + } + + cardRepository.delete(card); + } + + @Transactional + public void addCardToGroup(Long cardId, Long groupId) { + Group group = groupRepository.findById(groupId) + .orElseThrow(() -> new RuntimeException("해당 그룹을 찾을 수 없습니다.")); + + Card card = cardRepository.findById(cardId) + .orElseThrow(() -> new CardNotFoundException("해당 명함을 찾을 수 없습니다.")); + + group.addCard(card); + groupRepository.save(group); + } + + @Transactional(readOnly = true) + public List getCardsByGroup(Long groupId, Member member) { + Group group = groupRepository.findByIdAndMember(groupId, member) + .orElseThrow(() -> new RuntimeException("해당 그룹을 찾을 수 없습니다.")); + + return group.getCards().stream() + .map(CardResponseDto::fromEntity) + .collect(Collectors.toList()); + } + +} diff --git a/src/main/java/com/devcard/devcard/card/service/GroupService.java b/src/main/java/com/devcard/devcard/card/service/GroupService.java index 156ce8d..7e397c5 100644 --- a/src/main/java/com/devcard/devcard/card/service/GroupService.java +++ b/src/main/java/com/devcard/devcard/card/service/GroupService.java @@ -2,21 +2,40 @@ import com.devcard.devcard.auth.entity.Member; import com.devcard.devcard.card.dto.GroupResponseDto; +import com.devcard.devcard.card.entity.Card; import com.devcard.devcard.card.entity.Group; +import com.devcard.devcard.card.repository.CardRepository; import com.devcard.devcard.card.repository.GroupRepository; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - +import com.devcard.devcard.chat.dto.CreateRoomRequest; +import com.devcard.devcard.chat.service.ChatRoomService; +import com.devcard.devcard.chat.service.ChatService; +import java.util.Arrays; import java.util.List; import java.util.stream.Collectors; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.server.ResponseStatusException; @Service public class GroupService { + private static final Logger logger = LoggerFactory.getLogger(ChatService.class); + private final GroupRepository groupRepository; + private final CardRepository cardRepository; + private final ChatRoomService chatRoomService; - public GroupService(GroupRepository groupRepository) { + public GroupService( + GroupRepository groupRepository, + CardRepository cardRepository, + ChatRoomService chatRoomService + ) { this.groupRepository = groupRepository; + this.cardRepository = cardRepository; + this.chatRoomService = chatRoomService; } @Transactional @@ -31,7 +50,89 @@ public List getGroupsByMember(Member member) { List groups = groupRepository.findByMember(member); return groups.stream() - .map(group -> new GroupResponseDto(group.getId(), group.getName(), group.getCount())) - .collect(Collectors.toList()); + .map(group -> new GroupResponseDto(group.getId(), group.getName(), group.getCount())) + .collect(Collectors.toList()); + } + + @Transactional + public void addCardToGroup(Long groupId, Long cardId, Member member) { + // 현재 사용자의 그룹 조회 + Group group = groupRepository.findByIdAndMember(groupId, member) + .orElseThrow(() -> new IllegalArgumentException("해당 그룹이 존재하지 않거나 접근 권한이 없습니다.")); + + // 추가하려는 명함(Card) 조회 + Card card = cardRepository.findById(cardId) + .orElseThrow(() -> new IllegalArgumentException("해당 ID의 명함이 존재하지 않습니다.")); + + // 그룹에 이미 해당 카드가 포함되어 있는지 확인 + if (group.getCards().contains(card)) { + throw new IllegalArgumentException("이미 해당 그룹에 추가되어 있는 명함입니다."); + } + + // 상대방의 그룹에서 현재 사용자의 Card를 포함하는 그룹이 있는지 확인 + boolean isMutuallyAdded = groupRepository.existsByMemberAndCards_Id(card.getMember(), member.getId()); + + // 양쪽 모두 추가된 경우에만 채팅방 생성 + if (isMutuallyAdded) { + logger.debug( + "Chat room participants: Member ID = " + member.getId() + ", Card Owner ID = " + card.getMember().getId()); + CreateRoomRequest createRoomRequest = new CreateRoomRequest(Arrays.asList( + member.getId(), + card.getMember().getId() + )); + chatRoomService.createChatRoom(createRoomRequest); + } else { + logger.debug("Chat room not created: Mutual addition not satisfied."); + } + + // 카드 추가 + group.addCard(card); + groupRepository.save(group); + } + + @Transactional + public void deleteCardFromGroup(Long groupId, Long cardId, Member member) { + Group group = groupRepository.findByIdAndMember(groupId, member) + .orElseThrow(() -> new IllegalArgumentException("해당 그룹이 존재하지 않거나 접근 권한이 없습니다.")); + + Card card = cardRepository.findById(cardId) + .orElseThrow(() -> new IllegalArgumentException("해당 ID의 명함이 존재하지 않습니다.")); + + // 채팅방 제거 + chatRoomService.deleteChatRoomByParticipants(Arrays.asList( + member.getId(), + card.getMember().getId() + )); + + group.removeCard(card); // 그룹에서 명함을 제거 + groupRepository.save(group); // 변경사항 저장 } + + @Transactional + public void updateGroupName(Long groupId, String newName, Member member) { + Group group = groupRepository.findByIdAndMember(groupId, member) + .orElseThrow(() -> new ResponseStatusException( + HttpStatus.BAD_REQUEST, "해당 그룹이 존재하지 않거나 접근 권한이 없습니다." + )); + + group.setName(newName); + groupRepository.save(group); + } + + @Transactional + public void deleteGroup(Long groupId, Member member) { + Group group = groupRepository.findByIdAndMember(groupId, member) + .orElseThrow(() -> new IllegalArgumentException("해당 그룹이 존재하지 않거나 접근 권한이 없습니다.")); + + // 그룹에 포함된 모든 카드의 채팅방 삭제 + for (Card card : group.getCards()) { + chatRoomService.deleteChatRoomByParticipants(Arrays.asList( + member.getId(), + card.getMember().getId() + )); + } + + groupRepository.delete(group); // 그룹 삭제 + } + } diff --git a/src/main/java/com/devcard/devcard/card/service/QrServiceImpl.java b/src/main/java/com/devcard/devcard/card/service/QrServiceImpl.java index a847c5e..7e453ac 100644 --- a/src/main/java/com/devcard/devcard/card/service/QrServiceImpl.java +++ b/src/main/java/com/devcard/devcard/card/service/QrServiceImpl.java @@ -1,81 +1,36 @@ -package com.devcard.devcard.card.service; - -import com.google.zxing.BarcodeFormat; -import com.google.zxing.MultiFormatWriter; -import com.google.zxing.WriterException; -import com.google.zxing.client.j2se.MatrixToImageWriter; -import com.google.zxing.common.BitMatrix; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Service; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; - -@Service -public class QrServiceImpl implements QrService{ - - private static final int QR_SIZE_WIDTH = 200; - private static final int QR_SIZE_HEIGHT = 200; - - @Value("${qr.domain.uri}") - private String domainUri; - - @Value("${qr.code.directory}") - private String qrCodeDirectory; - - /** - * @param cardId QR로 만들 명함 ID - * @return QR 코드 IMAGE 파일 이름만 반환 - */ - @Override - public String createQr(Long cardId) throws WriterException, IOException { - - // QR URL - QR 코드 정보 URL - String url = generateQrUrl(cardId); - - // QR Code - BitMatrix: qr code 정보 생성 - BitMatrix bitMatrix = generateQrCode(url); - - // Setting QR Image File Name, Path - String qrFileName = generateQrFileName(cardId); - Path qrPath = generateQrFilePath(qrFileName); - - // Save QR - saveQrCodeImage(bitMatrix, qrPath); - - return domainUri + "qrcodes/" + qrFileName; - } - - private String generateQrUrl(Long cardId) { - return domainUri + "cards/" + cardId; - } - - private BitMatrix generateQrCode(String url) throws WriterException { - try { - return new MultiFormatWriter().encode(url, BarcodeFormat.QR_CODE, QR_SIZE_WIDTH, QR_SIZE_HEIGHT); - } catch (WriterException e) { - throw e; - } - } - - private String generateQrFileName(Long cardId) { - return "card_id_" + cardId + ".png"; - } - - private Path generateQrFilePath(String qrFileName) { - return Paths.get(qrCodeDirectory + qrFileName); - } - - private void saveQrCodeImage(BitMatrix bitMatrix, Path qrPath) throws IOException { - try (ByteArrayOutputStream out = new ByteArrayOutputStream()) { - MatrixToImageWriter.writeToStream(bitMatrix, "png", out); - Files.createDirectories(qrPath.getParent()); - Files.write(qrPath, out.toByteArray()); - } catch (IOException e) { - throw e; - } - } -} +package com.devcard.devcard.card.service; + +import com.google.zxing.BarcodeFormat; +import com.google.zxing.MultiFormatWriter; +import com.google.zxing.WriterException; +import com.google.zxing.client.j2se.MatrixToImageWriter; +import com.google.zxing.common.BitMatrix; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +@Service +public class QrServiceImpl { + + private static final int QR_SIZE_WIDTH = 200; + private static final int QR_SIZE_HEIGHT = 200; + + @Value("${qr.domain.uri}") + private String domainUri; + + public ByteArrayOutputStream generateQrImageStream(Long cardId) throws WriterException, IOException { + // QR URL 생성 + String url = domainUri + "shared/cards/" + cardId; + + // QR Code 생성 + BitMatrix bitMatrix = new MultiFormatWriter().encode(url, BarcodeFormat.QR_CODE, QR_SIZE_WIDTH, QR_SIZE_HEIGHT); + + // QR Code 이미지를 메모리 스트림에 저장 + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + MatrixToImageWriter.writeToStream(bitMatrix, "PNG", outputStream); + + return outputStream; + } +} diff --git a/src/main/java/com/devcard/devcard/chat/controller/page/ChatController.java b/src/main/java/com/devcard/devcard/chat/controller/page/ChatController.java index 977f3cd..45bee94 100644 --- a/src/main/java/com/devcard/devcard/chat/controller/page/ChatController.java +++ b/src/main/java/com/devcard/devcard/chat/controller/page/ChatController.java @@ -2,7 +2,6 @@ import com.devcard.devcard.auth.entity.Member; import com.devcard.devcard.auth.repository.MemberRepository; -import com.devcard.devcard.chat.service.ChatRoomService; import org.springframework.security.core.Authentication; import org.springframework.security.oauth2.core.user.OAuth2User; import org.springframework.stereotype.Controller; @@ -27,7 +26,7 @@ public ChatController(MemberRepository memberRepository) { /** * 채팅 목록 페이지로 이동하는 엔드포인트 - * @param model 뷰에 데이터를 전달하기 위한 모델 객체 + * @param model 뷰에 데이터를 전달하기 위한 모델 객체 * @param authentication 인증 정보를 포함하는 객체 (OAuth2 사용자 정보 포함) * @return 채팅 목록 페이지 템플릿 (chat-list.html)의 이름 */ @@ -46,8 +45,8 @@ public String getChatList(Model model, Authentication authentication) { /** * 특정 채팅방 페이지로 이동하는 엔드포인트 - * @param chatId 조회하려는 채팅방의 ID - * @param model 뷰에 데이터를 전달하기 위한 모델 객체 + * @param chatId 조회하려는 채팅방의 ID + * @param model 뷰에 데이터를 전달하기 위한 모델 객체 * @param authentication 인증 정보를 포함하는 객체 (OAuth2 사용자 정보 포함) * @return 특정 채팅방 페이지 템플릿 (chat-room.html)의 이름 */ diff --git a/src/main/java/com/devcard/devcard/chat/dto/ChatUserResponse.java b/src/main/java/com/devcard/devcard/chat/dto/ChatUserResponse.java index 7baa6df..ecfda58 100644 --- a/src/main/java/com/devcard/devcard/chat/dto/ChatUserResponse.java +++ b/src/main/java/com/devcard/devcard/chat/dto/ChatUserResponse.java @@ -1,7 +1,7 @@ package com.devcard.devcard.chat.dto; -public record ChatUserResponse ( - String nickname, +public record ChatUserResponse( + String name, String profileImage ) { diff --git a/src/main/java/com/devcard/devcard/chat/dto/CreateRoomRequest.java b/src/main/java/com/devcard/devcard/chat/dto/CreateRoomRequest.java index 26d6705..4a235e1 100644 --- a/src/main/java/com/devcard/devcard/chat/dto/CreateRoomRequest.java +++ b/src/main/java/com/devcard/devcard/chat/dto/CreateRoomRequest.java @@ -6,6 +6,15 @@ public class CreateRoomRequest { private List participantsId; + // 기본 생성자 + public CreateRoomRequest() { + } + + // 모든 필드 생성자 + public CreateRoomRequest(List participantsId) { + this.participantsId = participantsId; + } + public List getParticipantsId() { return participantsId; } diff --git a/src/main/java/com/devcard/devcard/chat/model/ChatMessage.java b/src/main/java/com/devcard/devcard/chat/model/ChatMessage.java index 75d33d5..512c692 100644 --- a/src/main/java/com/devcard/devcard/chat/model/ChatMessage.java +++ b/src/main/java/com/devcard/devcard/chat/model/ChatMessage.java @@ -1,6 +1,5 @@ package com.devcard.devcard.chat.model; -import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; diff --git a/src/main/java/com/devcard/devcard/chat/model/ChatRoom.java b/src/main/java/com/devcard/devcard/chat/model/ChatRoom.java index d215518..44a371a 100644 --- a/src/main/java/com/devcard/devcard/chat/model/ChatRoom.java +++ b/src/main/java/com/devcard/devcard/chat/model/ChatRoom.java @@ -2,9 +2,11 @@ import com.devcard.devcard.auth.entity.Member; import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; import jakarta.persistence.JoinTable; import jakarta.persistence.ManyToMany; import jakarta.persistence.Table; @@ -18,16 +20,21 @@ public class ChatRoom { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - @ManyToMany - @JoinTable(name = "chat_room_participants") + @ManyToMany(fetch = FetchType.EAGER) + @JoinTable( + name = "chat_room_participants", + joinColumns = @JoinColumn(name = "chat_room_id"), + inverseJoinColumns = @JoinColumn(name = "member_id") + ) private List participants; private LocalDateTime createdAt; - private String lastMessage = "메세지를 보내보세요."; // 기본값 설정 + private String lastMessage = "메세지를 보내보세요."; // 기본값 설정 private LocalDateTime lastMessageTime; public ChatRoom(List participants, LocalDateTime createdAt) { this.participants = participants; this.createdAt = createdAt; + this.lastMessageTime = createdAt; // 초기 시간 설정 } protected ChatRoom() { diff --git a/src/main/java/com/devcard/devcard/chat/repository/ChatRoomRepository.java b/src/main/java/com/devcard/devcard/chat/repository/ChatRoomRepository.java index 31672af..059155b 100644 --- a/src/main/java/com/devcard/devcard/chat/repository/ChatRoomRepository.java +++ b/src/main/java/com/devcard/devcard/chat/repository/ChatRoomRepository.java @@ -2,6 +2,7 @@ import com.devcard.devcard.chat.model.ChatRoom; import java.util.List; +import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; @@ -10,4 +11,20 @@ public interface ChatRoomRepository extends JpaRepository { @Query("SELECT cr FROM ChatRoom cr JOIN cr.participants p WHERE p.id = :userId") List findByParticipantId(@Param("userId") String userId); + + @Query( + "SELECT cr FROM ChatRoom cr " + + "WHERE SIZE(cr.participants) = :size " + + "AND cr.id IN (" + + " SELECT cr2.id FROM ChatRoom cr2 " + + " JOIN cr2.participants p " + + " WHERE p.id IN :participantsId " + + " GROUP BY cr2.id " + + " HAVING COUNT(p.id) = :size" + + ")" + ) + Optional findByExactParticipants( + @Param("participantsId") List participantsId, + @Param("size") int size + ); } diff --git a/src/main/java/com/devcard/devcard/chat/repository/ChatUserRepository.java b/src/main/java/com/devcard/devcard/chat/repository/ChatUserRepository.java deleted file mode 100644 index 49aea8a..0000000 --- a/src/main/java/com/devcard/devcard/chat/repository/ChatUserRepository.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.devcard.devcard.chat.repository; - -import com.devcard.devcard.auth.entity.Member; -import java.util.List; -import org.springframework.data.jpa.repository.JpaRepository; - -public interface ChatUserRepository extends JpaRepository { - - List findByIdIn(List participantsId); -} diff --git a/src/main/java/com/devcard/devcard/chat/service/ChatRoomService.java b/src/main/java/com/devcard/devcard/chat/service/ChatRoomService.java index acaa062..ef906a7 100644 --- a/src/main/java/com/devcard/devcard/chat/service/ChatRoomService.java +++ b/src/main/java/com/devcard/devcard/chat/service/ChatRoomService.java @@ -1,8 +1,11 @@ package com.devcard.devcard.chat.service; import static com.devcard.devcard.chat.util.Constants.CHAT_ROOM_NOT_FOUND; +import static com.devcard.devcard.chat.util.Constants.CHAT_ROOM_NOT_FOUND_BY_PARTICIPANTS; +import static com.devcard.devcard.chat.util.Constants.DUPLICATE_CHAT_ROOM_ERROR; import com.devcard.devcard.auth.entity.Member; +import com.devcard.devcard.auth.repository.MemberRepository; import com.devcard.devcard.chat.dto.ChatMessageResponse; import com.devcard.devcard.chat.dto.ChatRoomListResponse; import com.devcard.devcard.chat.dto.ChatRoomResponse; @@ -13,7 +16,6 @@ import com.devcard.devcard.chat.model.ChatRoom; import com.devcard.devcard.chat.repository.ChatRepository; import com.devcard.devcard.chat.repository.ChatRoomRepository; -import com.devcard.devcard.chat.repository.ChatUserRepository; import java.time.LocalDateTime; import java.util.List; import java.util.stream.Collectors; @@ -30,16 +32,16 @@ public class ChatRoomService { private final ChatRoomRepository chatRoomRepository; private final ChatRepository chatRepository; - private final ChatUserRepository chatUserRepository; + private final MemberRepository memberRepository; public ChatRoomService( ChatRoomRepository chatRoomRepository, ChatRepository chatRepository, - ChatUserRepository chatUserRepository + MemberRepository memberRepository ) { this.chatRoomRepository = chatRoomRepository; this.chatRepository = chatRepository; - this.chatUserRepository = chatUserRepository; + this.memberRepository = memberRepository; } /** @@ -48,11 +50,22 @@ public ChatRoomService( * @return 생성된 채팅방 정보 */ public CreateRoomResponse createChatRoom(CreateRoomRequest createRoomRequest) { - // jpa를 이용해 ChatUser 리스트 가져오기 - List participants = chatUserRepository.findByIdIn(createRoomRequest.getParticipantsId()); - ChatRoom chatRoom = new ChatRoom(participants, LocalDateTime.now()); // chatRoom생성 - chatRoomRepository.save(chatRoom); // db에 저장 - return makeCreateChatRoomResponse(chatRoom); // Response로 변환 + // 참여자 ID를 기반으로 Member 리스트 가져오기 + List participants = memberRepository.findByIdIn(createRoomRequest.getParticipantsId()); + int participantSize = participants.size(); + + // 동일한 참여자 구성의 채팅방이 있는지 확인 + chatRoomRepository.findByExactParticipants(createRoomRequest.getParticipantsId(), participantSize) + .ifPresent(existingRoom -> { + throw new IllegalArgumentException(DUPLICATE_CHAT_ROOM_ERROR); + }); + + // 새로운 채팅방 생성 + ChatRoom chatRoom = new ChatRoom(participants, LocalDateTime.now()); + chatRoomRepository.save(chatRoom); // DB에 저장 + + // Response 변환 및 반환 + return makeCreateChatRoomResponse(chatRoom); } @@ -122,7 +135,7 @@ public ChatRoomResponse getChatRoomById(String chatId) { } /** - * 채팅방 삭제 + * chatId를 이용해 채팅방 삭제 * @param chatId 채팅방 ID */ public void deleteChatRoom(String chatId) { @@ -139,6 +152,26 @@ public void deleteChatRoom(String chatId) { chatRoomRepository.deleteById(chatRoomId); } + /** + * 참여자 ID를 이용해 채팅방 삭제 + * @param participantsId 채팅방에 참여하는 모든 유저의 ID List + */ + public void deleteChatRoomByParticipants(List participantsId) { + // 참여자 수 계산 + int size = participantsId.size(); + + // 정확히 일치하는 채팅방 조회 + ChatRoom chatRoom = chatRoomRepository.findByExactParticipants(participantsId, size) + .orElseThrow(() -> new ChatRoomNotFoundException( + CHAT_ROOM_NOT_FOUND_BY_PARTICIPANTS + participantsId.toString())); + + // 관련된 메시지 삭제 + chatRepository.deleteByChatRoomId(chatRoom.getId()); + + // 채팅방 삭제 + chatRoomRepository.delete(chatRoom); + } + /** * 특정 채팅방이 존재하는지 확인 * @param chatId 채팅방 ID diff --git a/src/main/java/com/devcard/devcard/chat/service/ChatService.java b/src/main/java/com/devcard/devcard/chat/service/ChatService.java index d28115a..aa4dd6d 100644 --- a/src/main/java/com/devcard/devcard/chat/service/ChatService.java +++ b/src/main/java/com/devcard/devcard/chat/service/ChatService.java @@ -1,6 +1,11 @@ package com.devcard.devcard.chat.service; import static com.devcard.devcard.chat.util.Constants.CHAT_ROOM_NOT_FOUND; +import static com.devcard.devcard.chat.util.Constants.EMPTY_MESSAGE; +import static com.devcard.devcard.chat.util.Constants.MEMBER_NOT_FOUND; +import static com.devcard.devcard.chat.util.Constants.NO_QUERY_PARAMETER; +import static com.devcard.devcard.chat.util.Constants.NUMBER_FORMAT_ERROR; +import static com.devcard.devcard.chat.util.Constants.USER_NOT_IN_CHAT_ROOM; import com.devcard.devcard.auth.entity.Member; import com.devcard.devcard.auth.repository.MemberRepository; @@ -14,7 +19,6 @@ import java.time.LocalDateTime; import java.util.List; import java.util.Objects; -import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.CopyOnWriteArrayList; @@ -47,7 +51,11 @@ public class ChatService { private final ChatRoomRepository chatRoomRepository; private final MemberRepository memberRepository; - public ChatService(ChatRepository chatRepository, ChatRoomRepository chatRoomRepository, MemberRepository memberRepository) { + public ChatService( + ChatRepository chatRepository, + ChatRoomRepository chatRoomRepository, + MemberRepository memberRepository + ) { this.chatRepository = chatRepository; this.chatRoomRepository = chatRoomRepository; this.memberRepository = memberRepository; @@ -60,6 +68,11 @@ public ChatService(ChatRepository chatRepository, ChatRoomRepository chatRoomRep * @param message 전송하려는 메시지 */ public void handleIncomingMessage(Long chatId, Long userId, String message) { + // message가 null이거나 빈 문자열인 경우 예외 처리 + if (message == null || message.trim().isEmpty()) { + throw new IllegalArgumentException(EMPTY_MESSAGE); + } + // ChatRoom 조회 ChatRoom chatRoom = chatRoomRepository.findById(chatId) .orElseThrow(() -> new ChatRoomNotFoundException(CHAT_ROOM_NOT_FOUND + chatId)); @@ -70,7 +83,7 @@ public void handleIncomingMessage(Long chatId, Long userId, String message) { if (!isParticipant) { logger.warn("사용자가 채팅방에 참여하지 않음: userId={}, chatId={}", userId, chatId); - throw new IllegalArgumentException("사용자가 해당 채팅방의 참여자가 아닙니다."); + throw new IllegalArgumentException(USER_NOT_IN_CHAT_ROOM); } // 메시지 저장 @@ -158,8 +171,14 @@ public List getChatRoomSessions(Long chatId) { public String extractMessage(String payload) { JSONParser parser = new JSONParser(); try { - JSONObject jsonObject = (JSONObject) parser.parse(payload); - return jsonObject.getAsString("content"); + Object obj = parser.parse(payload); + if (obj instanceof JSONObject) { // JSONObject로 캐스팅 가능한지 확인 + JSONObject jsonObject = (JSONObject) obj; + return jsonObject.getAsString("content"); + } else { + logger.error("유효하지 않은 JSON 형식: {}", payload); + return null; + } } catch (ParseException e) { logger.error("payload에서 message 추출 실패: {}", payload, e); return null; // 예외 발생 시 null 반환 @@ -193,13 +212,13 @@ public Long extractUserIdFromSession(WebSocketSession session) { * @return 해당 파라미터 값 (존재하지 않으면 null 반환) */ private Long extractParamFromUri(String uri, String paramName) { - // e.g. `ws://localhost:8080/ws?chatId=1&userId=1` 입력의 경우, + // e.g. `ws://3.34.144.148:8080/ws?chatId=1&userId=1` 입력의 경우, try { // "?"로 나누어 쿼리 파라미터 부분만 가져옴 (e.g. `chatId=1&userId=1`) String[] parts = uri.split("\\?"); if (parts.length < 2) { logger.warn("쿼리 파라미터 없음: {}", uri); - throw new IllegalArgumentException(paramName + " 파라미터가 없음"); + throw new IllegalArgumentException(paramName + NO_QUERY_PARAMETER); } // 쿼리 부분을 "&"로 나누어 매개변수 배열로 변환 return Stream.of(parts[1].split("&")) // 쿼리 문자열에서 &로 분리 (e.g. `["chatId=1", "userId=1"]`) @@ -211,13 +230,27 @@ private Long extractParamFromUri(String uri, String paramName) { .orElse(null); // 없으면 null 반환 } catch (NumberFormatException e) { logger.error("URI에서 {} 추출 실패: {}", paramName, uri, e); - throw new IllegalArgumentException(paramName + " 추출 중 숫자 형식 오류"); + throw new IllegalArgumentException(paramName + NUMBER_FORMAT_ERROR); } } + /** + * ID를 통해 유저의 프로필을 반환 + * @param userId 유저의 ID + * @return 해당 유저의 name과 이미지 반환 + */ public ChatUserResponse getUserProfileById(String userId) { - Member member = memberRepository.findById(Integer.parseInt(userId)) - .orElseThrow(() -> new IllegalArgumentException("멤버를 찾을 수 없습니다.")); - return new ChatUserResponse(member.getNickname(), member.getProfileImg()); + Member member = memberRepository.findById(Long.parseLong(userId)) + .orElseThrow(() -> new IllegalArgumentException(MEMBER_NOT_FOUND + userId)); + + // member.getUserName()이 null일 경우 member.getNickname()을 사용 + String name = (member.getUsername() != null) ? member.getUsername() : member.getNickname(); + + return new ChatUserResponse(name, member.getProfileImg()); + } + + // getter + public ConcurrentMap> getChatRoomSessions() { + return chatRoomSessions; } } diff --git a/src/main/java/com/devcard/devcard/chat/util/Constants.java b/src/main/java/com/devcard/devcard/chat/util/Constants.java index d84c55c..9422170 100644 --- a/src/main/java/com/devcard/devcard/chat/util/Constants.java +++ b/src/main/java/com/devcard/devcard/chat/util/Constants.java @@ -4,4 +4,15 @@ public class Constants { // 채팅방 상수 public static final String CHAT_ROOM_NOT_FOUND = "채팅방을 다음의 id로 찾을 수 없습니다. id: "; + public static final String CHAT_ROOM_NOT_FOUND_BY_PARTICIPANTS = "해당 참여자 ID 목록으로 채팅방을 찾을 수 없습니다. 참여자 ID 목록: "; + public static final String USER_NOT_IN_CHAT_ROOM = "사용자가 해당 채팅방의 참여자가 아닙니다."; + public static final String EMPTY_MESSAGE = "메시지가 비어 있습니다."; + public static final String DUPLICATE_CHAT_ROOM_ERROR = "이미 동일한 참여자로 구성된 채팅방이 존재합니다."; + + // 멤버 상수 + public static final String MEMBER_NOT_FOUND = "멤버를 다음의 id로 찾을 수 없습니다. id: "; + + // 오류 상수 + public static final String NO_QUERY_PARAMETER = " 쿼리 파라미터가 없습니다."; + public static final String NUMBER_FORMAT_ERROR = " 추출 중 숫자 형식 오류가 발생했습니다."; } diff --git a/src/main/java/com/devcard/devcard/config/WebConfig.java b/src/main/java/com/devcard/devcard/config/WebConfig.java index 900eb5b..9f65eb8 100644 --- a/src/main/java/com/devcard/devcard/config/WebConfig.java +++ b/src/main/java/com/devcard/devcard/config/WebConfig.java @@ -2,6 +2,7 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpHeaders; import org.springframework.web.servlet.config.annotation.*; @Configuration @@ -9,9 +10,12 @@ public class WebConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { - registry.addMapping("/api/**") - .allowedOrigins("http://localhost:8080") - .allowedMethods("GET", "POST", "PUT", "DELETE") - .allowCredentials(true); + registry.addMapping("/**") // 모든 경로에 대해 CORS 설정 적용 + .allowedOriginPatterns("*") // 모든 도메인에서 오는 요청 허용 + .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS", "HEAD") // 허용할 HTTP 메서드 목록 + .allowedHeaders("*") // 모든 요청 헤더 허용 + .exposedHeaders(HttpHeaders.LOCATION) // 응답 헤더 중 'Location' 헤더를 클라이언트에서 사용할 수 있도록 허용 + .allowCredentials(true) // 자격 증명(쿠키, 인증 정보 등)을 포함한 요청 허용 + .maxAge(3600); // pre-flight 요청의 캐시 시간을 3600초(1시간)로 설정 } } diff --git a/src/main/java/com/devcard/devcard/mypage/controller/page/MyPageController.java b/src/main/java/com/devcard/devcard/mypage/controller/page/MyPageController.java new file mode 100644 index 0000000..63a959f --- /dev/null +++ b/src/main/java/com/devcard/devcard/mypage/controller/page/MyPageController.java @@ -0,0 +1,68 @@ +package com.devcard.devcard.mypage.controller.page; + +import com.devcard.devcard.auth.entity.Member; +import com.devcard.devcard.auth.repository.MemberRepository; +import com.devcard.devcard.mypage.service.NoticeService; +import com.devcard.devcard.mypage.service.QnAService; +import org.springframework.security.core.Authentication; +import org.springframework.security.oauth2.core.user.OAuth2User; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; + +@Controller +public class MyPageController { + private final NoticeService noticeService; + private final QnAService qnAService; + private final MemberRepository memberRepository; + + public MyPageController(NoticeService noticeService, QnAService qnAService, + MemberRepository memberRepository) { + this.noticeService = noticeService; + this.qnAService = qnAService; + this.memberRepository = memberRepository; + } + + @GetMapping("/mypage/notice") + public String getNoticeList(Model model) { + model.addAttribute("noticeList", noticeService.getNoticeList()); + return "notice-list"; + } + + @GetMapping("/mypage/notice/{id}") + public String getNotice(@PathVariable(name = "id") Long id, Model model) { + model.addAttribute("notice", noticeService.getNotice(id)); + return "notice-detail"; + } + + @GetMapping("/mypage/qna") + public String getQnAList(Model model) { + model.addAttribute("qnaList", qnAService.getQnAList()); + return "qna-list"; + } + + @GetMapping("/mypage/qna/{id}") + public String getQnA(@PathVariable(name = "id") Long id, Model model) { + model.addAttribute("qna", qnAService.getQnA(id)); + return "qna-detail"; + } + + @GetMapping("/mypage/qna/create") + public String getQnACreate(Model model, Authentication authentication) { + OAuth2User oAuth2User = (OAuth2User) authentication.getPrincipal(); + String githubId = String.valueOf(oAuth2User.getAttributes().get("id")); + + // GitHub ID로 사용자를 찾아서 등록 여부 확인 + Member member = memberRepository.findByGithubId(githubId); + + model.addAttribute("member", member); + return "qna-create-new"; + } + + @GetMapping("/mypage/qna/create/{id}") + public String getQnAUpdate(@PathVariable(name = "id") Long id, Model model) { + model.addAttribute("qna", qnAService.getQnA(id)); + return "qna-create-update"; + } +} diff --git a/src/main/java/com/devcard/devcard/mypage/controller/rest/NoticeController.java b/src/main/java/com/devcard/devcard/mypage/controller/rest/NoticeController.java new file mode 100644 index 0000000..465a81f --- /dev/null +++ b/src/main/java/com/devcard/devcard/mypage/controller/rest/NoticeController.java @@ -0,0 +1,52 @@ +package com.devcard.devcard.mypage.controller.rest; + +import com.devcard.devcard.mypage.dto.NoticeRequest; +import com.devcard.devcard.mypage.dto.NoticeResponse; +import com.devcard.devcard.mypage.dto.NoticeUpdateRequest; +import com.devcard.devcard.mypage.service.NoticeService; +import java.util.List; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/notice") +public class NoticeController { + + private final NoticeService noticeService; + + public NoticeController(NoticeService noticeService) { + this.noticeService = noticeService; + } + + @GetMapping + public ResponseEntity> getAllNotice(){ + return ResponseEntity.status(201).body(noticeService.getNoticeList()); + } + + @GetMapping("/{id}") + public ResponseEntity getNotice(@PathVariable("id") Long id){ + return ResponseEntity.ok(noticeService.getNotice(id)); + } + + @PostMapping("") + public ResponseEntity addNotice(@RequestBody NoticeRequest noticeRequest){ + return ResponseEntity.ok(noticeService.addNotice(noticeRequest)); + } + + @PutMapping("") + public ResponseEntity updateNotice(@RequestBody NoticeUpdateRequest noticeUpdateRequest){ + return ResponseEntity.ok(noticeService.updateNotice(noticeUpdateRequest)); + } + + @DeleteMapping("/{id}") + public ResponseEntity deleteNotice(@PathVariable("id") Long id){ + return ResponseEntity.ok(noticeService.deleteNotice(id)); + } +} diff --git a/src/main/java/com/devcard/devcard/mypage/controller/rest/QnAController.java b/src/main/java/com/devcard/devcard/mypage/controller/rest/QnAController.java new file mode 100644 index 0000000..97c6e8f --- /dev/null +++ b/src/main/java/com/devcard/devcard/mypage/controller/rest/QnAController.java @@ -0,0 +1,56 @@ +package com.devcard.devcard.mypage.controller.rest; + +import com.devcard.devcard.mypage.dto.NoticeRequest; +import com.devcard.devcard.mypage.dto.NoticeResponse; +import com.devcard.devcard.mypage.dto.NoticeUpdateRequest; +import com.devcard.devcard.mypage.dto.QnAListResponse; +import com.devcard.devcard.mypage.dto.QnARequest; +import com.devcard.devcard.mypage.dto.QnAResponse; +import com.devcard.devcard.mypage.dto.QnAUpdateRequest; +import com.devcard.devcard.mypage.service.QnAService; +import java.util.List; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/qna") +public class QnAController { + + private final QnAService qnAService; + + public QnAController(QnAService qnAService) { + this.qnAService = qnAService; + } + + @GetMapping + public ResponseEntity> getAllQnA(){ + return ResponseEntity.status(201).body(qnAService.getQnAList()); + } + + @GetMapping("/{id}") + public ResponseEntity getQnA(@PathVariable("id") Long id){ + return ResponseEntity.ok(qnAService.getQnA(id)); + } + + @PostMapping("") + public ResponseEntity addQnA(@RequestBody QnARequest qnARequest){ + return ResponseEntity.ok(qnAService.addQnA(qnARequest)); + } + + @PutMapping("") + public ResponseEntity updateQnA(@RequestBody QnAUpdateRequest qnAUpdateRequest){ + return ResponseEntity.ok(qnAService.updateQnA(qnAUpdateRequest)); + } + + @DeleteMapping("/{id}") + public ResponseEntity deleteQnA(@PathVariable("id") Long id){ + return ResponseEntity.ok(qnAService.deleteQnA(id)); + } +} diff --git a/src/main/java/com/devcard/devcard/mypage/dto/NoticeRequest.java b/src/main/java/com/devcard/devcard/mypage/dto/NoticeRequest.java new file mode 100644 index 0000000..a6f27dc --- /dev/null +++ b/src/main/java/com/devcard/devcard/mypage/dto/NoticeRequest.java @@ -0,0 +1,7 @@ +package com.devcard.devcard.mypage.dto; + +import java.time.LocalDateTime; + +public record NoticeRequest(String title, String content, LocalDateTime timestamp) { + +} diff --git a/src/main/java/com/devcard/devcard/mypage/dto/NoticeResponse.java b/src/main/java/com/devcard/devcard/mypage/dto/NoticeResponse.java new file mode 100644 index 0000000..4ff3e28 --- /dev/null +++ b/src/main/java/com/devcard/devcard/mypage/dto/NoticeResponse.java @@ -0,0 +1,7 @@ +package com.devcard.devcard.mypage.dto; + +import java.time.LocalDateTime; + +public record NoticeResponse(Long id, String title, String content, LocalDateTime timestamp) { + +} diff --git a/src/main/java/com/devcard/devcard/mypage/dto/NoticeUpdateRequest.java b/src/main/java/com/devcard/devcard/mypage/dto/NoticeUpdateRequest.java new file mode 100644 index 0000000..b24b88f --- /dev/null +++ b/src/main/java/com/devcard/devcard/mypage/dto/NoticeUpdateRequest.java @@ -0,0 +1,7 @@ +package com.devcard.devcard.mypage.dto; + +import java.time.LocalDateTime; + +public record NoticeUpdateRequest (Long id, String title, String content, LocalDateTime timestamp) { + +} diff --git a/src/main/java/com/devcard/devcard/mypage/dto/QnAAnswerDTO.java b/src/main/java/com/devcard/devcard/mypage/dto/QnAAnswerDTO.java new file mode 100644 index 0000000..a7acd3f --- /dev/null +++ b/src/main/java/com/devcard/devcard/mypage/dto/QnAAnswerDTO.java @@ -0,0 +1,5 @@ +package com.devcard.devcard.mypage.dto; + +public record QnAAnswerDTO(Long id, String answer) { + +} diff --git a/src/main/java/com/devcard/devcard/mypage/dto/QnAListResponse.java b/src/main/java/com/devcard/devcard/mypage/dto/QnAListResponse.java new file mode 100644 index 0000000..d3c0cd4 --- /dev/null +++ b/src/main/java/com/devcard/devcard/mypage/dto/QnAListResponse.java @@ -0,0 +1,11 @@ +package com.devcard.devcard.mypage.dto; + +import java.time.LocalDateTime; + +public record QnAListResponse(Long id, + String name, + String questionTitle, + boolean answerCompleted, + LocalDateTime questionTimestamp) { + +} diff --git a/src/main/java/com/devcard/devcard/mypage/dto/QnARequest.java b/src/main/java/com/devcard/devcard/mypage/dto/QnARequest.java new file mode 100644 index 0000000..81decbd --- /dev/null +++ b/src/main/java/com/devcard/devcard/mypage/dto/QnARequest.java @@ -0,0 +1,5 @@ +package com.devcard.devcard.mypage.dto; + +public record QnARequest(String name, String questionTitle, String questionContent) { + +} diff --git a/src/main/java/com/devcard/devcard/mypage/dto/QnAResponse.java b/src/main/java/com/devcard/devcard/mypage/dto/QnAResponse.java new file mode 100644 index 0000000..5a120a1 --- /dev/null +++ b/src/main/java/com/devcard/devcard/mypage/dto/QnAResponse.java @@ -0,0 +1,13 @@ +package com.devcard.devcard.mypage.dto; + +import java.time.LocalDateTime; + +public record QnAResponse(Long id, + String name, + String questionTitle, + String questionContent, + String answer, + LocalDateTime questionTimestamp, + LocalDateTime answerTimestamp, + boolean answerCompleted) { +} diff --git a/src/main/java/com/devcard/devcard/mypage/dto/QnAUpdateRequest.java b/src/main/java/com/devcard/devcard/mypage/dto/QnAUpdateRequest.java new file mode 100644 index 0000000..98487f1 --- /dev/null +++ b/src/main/java/com/devcard/devcard/mypage/dto/QnAUpdateRequest.java @@ -0,0 +1,5 @@ +package com.devcard.devcard.mypage.dto; + +public record QnAUpdateRequest(Long id, String questionTitle, String questionContent) { + +} diff --git a/src/main/java/com/devcard/devcard/mypage/entity/Notice.java b/src/main/java/com/devcard/devcard/mypage/entity/Notice.java new file mode 100644 index 0000000..2672820 --- /dev/null +++ b/src/main/java/com/devcard/devcard/mypage/entity/Notice.java @@ -0,0 +1,58 @@ +package com.devcard.devcard.mypage.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import java.time.LocalDateTime; + +@Entity +@Table(name = "notice") +public class Notice { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String title; + + @Column(columnDefinition = "MEDIUMTEXT") + private String content; + + private LocalDateTime timestamp; + + public Notice() { + + } + + public Notice(String title, String content, LocalDateTime timestamp) { + this.title = title; + this.content = content; + this.timestamp = timestamp; + } + + public Notice(Long id, String title, String content, LocalDateTime timestamp) { + this.id = id; + this.title = title; + this.content = content; + this.timestamp = timestamp; + } + + public String getTitle() { + return title; + } + + public String getContent() { + return content; + } + + public LocalDateTime getTimestamp() { + return timestamp; + } + + public Long getId() { + return id; + } +} diff --git a/src/main/java/com/devcard/devcard/mypage/entity/QnA.java b/src/main/java/com/devcard/devcard/mypage/entity/QnA.java new file mode 100644 index 0000000..757ceb0 --- /dev/null +++ b/src/main/java/com/devcard/devcard/mypage/entity/QnA.java @@ -0,0 +1,84 @@ +package com.devcard.devcard.mypage.entity; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import java.time.LocalDateTime; + +@Entity(name = "qna") +public class QnA { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String name; + + private String questionTitle; + + private String questionContent; + + private String answer; + + private LocalDateTime questionTimestamp; + + private LocalDateTime answerTimestamp; + + private boolean answerCompleted; + + public QnA(){ + + } + + public QnA(String name, String questionTitle, String questionContent, LocalDateTime questionTimestamp) { + this.name = name; + this.questionTitle = questionTitle; + this.questionContent = questionContent; + this.questionTimestamp = questionTimestamp; + this.answerCompleted = false; + } + + public Long getId() { + return id; + } + + public String getName() { + return name; + } + + public String getQuestionTitle() { + return questionTitle; + } + + public String getQuestionContent() { + return questionContent; + } + + public String getAnswer() { + return answer; + } + + public LocalDateTime getQuestionTimestamp() { + return questionTimestamp; + } + + public LocalDateTime getAnswerTimestamp() { + return answerTimestamp; + } + + public boolean isAnswerCompleted() { + return answerCompleted; + } + + public void updateByRequest(String updateQuestionTitle, String updateQuestionContent){ + this.questionTitle = updateQuestionTitle; + this.questionContent = updateQuestionContent; + } + + public void updateAnswer(String updateAnswer){ + this.answer = updateAnswer; + this.answerTimestamp = LocalDateTime.now(); + this.answerCompleted = true; + } +} diff --git a/src/main/java/com/devcard/devcard/mypage/repository/NoticeRepository.java b/src/main/java/com/devcard/devcard/mypage/repository/NoticeRepository.java new file mode 100644 index 0000000..51cf9c2 --- /dev/null +++ b/src/main/java/com/devcard/devcard/mypage/repository/NoticeRepository.java @@ -0,0 +1,11 @@ +package com.devcard.devcard.mypage.repository; + +import com.devcard.devcard.mypage.entity.Notice; +import java.util.List; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface NoticeRepository extends JpaRepository { + List findAllByOrderByTimestampDesc(); + + Notice findNoticeById(Long id); +} diff --git a/src/main/java/com/devcard/devcard/mypage/repository/QnARepository.java b/src/main/java/com/devcard/devcard/mypage/repository/QnARepository.java new file mode 100644 index 0000000..6bfbeaa --- /dev/null +++ b/src/main/java/com/devcard/devcard/mypage/repository/QnARepository.java @@ -0,0 +1,11 @@ +package com.devcard.devcard.mypage.repository; + +import com.devcard.devcard.mypage.entity.QnA; +import java.util.List; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface QnARepository extends JpaRepository { + List findAllByOrderByQuestionTimestampDesc(); + + QnA findQnAById(Long id); +} diff --git a/src/main/java/com/devcard/devcard/mypage/service/NoticeService.java b/src/main/java/com/devcard/devcard/mypage/service/NoticeService.java new file mode 100644 index 0000000..876cc09 --- /dev/null +++ b/src/main/java/com/devcard/devcard/mypage/service/NoticeService.java @@ -0,0 +1,50 @@ +package com.devcard.devcard.mypage.service; + +import com.devcard.devcard.mypage.dto.NoticeRequest; +import com.devcard.devcard.mypage.dto.NoticeResponse; +import com.devcard.devcard.mypage.dto.NoticeUpdateRequest; +import com.devcard.devcard.mypage.entity.Notice; +import com.devcard.devcard.mypage.repository.NoticeRepository; +import java.time.LocalDateTime; +import java.util.List; +import org.springframework.stereotype.Service; + +@Service +public class NoticeService { + private final NoticeRepository noticeRepository; + + public NoticeService(NoticeRepository noticeRepository) { + this.noticeRepository = noticeRepository; + } + + public List getNoticeList() { + List noticeList = noticeRepository.findAllByOrderByTimestampDesc(); + return noticeList.stream().map(notice -> new NoticeResponse( + notice.getId(), + notice.getTitle(), + notice.getContent(), + notice.getTimestamp() + )).toList(); + } + + public NoticeResponse getNotice(Long id) { + Notice notice = noticeRepository.findNoticeById(id); + return new NoticeResponse(notice.getId(), notice.getTitle(), notice.getContent(), notice.getTimestamp()); + } + + public NoticeResponse addNotice(NoticeRequest noticeRequest) { + Notice notice = noticeRepository.save(new Notice(noticeRequest.title(), noticeRequest.content(), LocalDateTime.now())); + return new NoticeResponse(notice.getId(), notice.getTitle(), notice.getContent(), notice.getTimestamp()); + } + + public NoticeResponse updateNotice(NoticeUpdateRequest noticeUpdateRequest) { + Notice notice = noticeRepository.save(new Notice(noticeUpdateRequest.id(), noticeUpdateRequest.title(), noticeUpdateRequest.content(), LocalDateTime.now())); + return new NoticeResponse(notice.getId(), notice.getTitle(), notice.getContent(), notice.getTimestamp()); + } + + public NoticeResponse deleteNotice(Long id) { + Notice deleteNotice = noticeRepository.findNoticeById(id); + noticeRepository.delete(deleteNotice); + return new NoticeResponse(deleteNotice.getId(), deleteNotice.getTitle(), deleteNotice.getContent(), deleteNotice.getTimestamp()); + } +} diff --git a/src/main/java/com/devcard/devcard/mypage/service/QnAService.java b/src/main/java/com/devcard/devcard/mypage/service/QnAService.java new file mode 100644 index 0000000..a527c7b --- /dev/null +++ b/src/main/java/com/devcard/devcard/mypage/service/QnAService.java @@ -0,0 +1,62 @@ +package com.devcard.devcard.mypage.service; + +import com.devcard.devcard.mypage.dto.QnAAnswerDTO; +import com.devcard.devcard.mypage.dto.QnAListResponse; +import com.devcard.devcard.mypage.dto.QnARequest; +import com.devcard.devcard.mypage.dto.QnAResponse; +import com.devcard.devcard.mypage.dto.QnAUpdateRequest; +import com.devcard.devcard.mypage.entity.QnA; +import com.devcard.devcard.mypage.repository.QnARepository; +import java.time.LocalDateTime; +import java.util.List; +import org.springframework.stereotype.Service; + +@Service +public class QnAService { + private final QnARepository qnARepository; + + public QnAService(QnARepository qnARepository) { + this.qnARepository = qnARepository; + } + public List getQnAList() { + List qnaList = qnARepository.findAllByOrderByQuestionTimestampDesc(); + return qnaList.stream().map(qna -> new QnAListResponse( + qna.getId(), + qna.getName(), + qna.getQuestionTitle(), + qna.isAnswerCompleted(), + qna.getQuestionTimestamp() + )).toList(); + } + + public QnAResponse getQnA(Long id) { + QnA qna = qnARepository.findQnAById(id); + return new QnAResponse(qna.getId(), qna.getName(), qna.getQuestionTitle(), qna.getQuestionContent(), qna.getAnswer(), qna.getQuestionTimestamp(), qna.getAnswerTimestamp(), qna.isAnswerCompleted()); + } + + public QnAResponse addQnA(QnARequest qnARequest) { + QnA qna = qnARepository.save(new QnA(qnARequest.name(), qnARequest.questionTitle(), qnARequest.questionContent(), + LocalDateTime.now())); + return new QnAResponse(qna.getId(), qna.getName(), qna.getQuestionTitle(), qna.getQuestionContent(), qna.getAnswer(), qna.getQuestionTimestamp(), qna.getAnswerTimestamp(), qna.isAnswerCompleted()); + } + + public QnAResponse updateQnA(QnAUpdateRequest qnAUpdateRequest) { + QnA qna = qnARepository.findQnAById(qnAUpdateRequest.id()); + qna.updateByRequest(qnAUpdateRequest.questionTitle(), qnAUpdateRequest.questionContent()); + qnARepository.save(qna); + return new QnAResponse(qna.getId(), qna.getName(), qna.getQuestionTitle(), qna.getQuestionContent(), qna.getAnswer(), qna.getQuestionTimestamp(), qna.getAnswerTimestamp(), qna.isAnswerCompleted()); + } + + public QnAResponse deleteQnA(Long id) { + QnA qna = qnARepository.findQnAById(id); + qnARepository.delete(qna); + return new QnAResponse(qna.getId(), qna.getName(), qna.getQuestionTitle(), qna.getQuestionContent(), qna.getAnswer(), qna.getQuestionTimestamp(), qna.getAnswerTimestamp(), qna.isAnswerCompleted()); + } + + public QnAResponse updateAnswer(QnAAnswerDTO qnAAnswerDTO) { + QnA qna = qnARepository.findQnAById(qnAAnswerDTO.id()); + qna.updateAnswer(qnAAnswerDTO.answer()); + return new QnAResponse(qna.getId(), qna.getName(), qna.getQuestionTitle(), qna.getQuestionContent(), qna.getAnswer(), qna.getQuestionTimestamp(), qna.getAnswerTimestamp(), qna.isAnswerCompleted()); + } + +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 8c7f38b..6a0f0d9 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,31 +1,28 @@ spring.application.name=DevCard # sql -spring.sql.init.mode=always +spring.sql.init.mode=never spring.sql.init.schema-locations=classpath:schema.sql spring.sql.init.data-locations=classpath:data.sql # JPA spring.jpa.hibernate.ddl-auto=update spring.jpa.defer-datasource-initialization=true -spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.H2Dialect +spring.jpa.database-platform=org.hibernate.dialect.MySQL8Dialect spring.jpa.properties.hibernate.format_sql=true spring.jpa.show-sql=true # db -spring.h2.console.enabled=true -spring.datasource.url=jdbc:h2:mem:test;MODE=MYSQL;DB_CLOSE_DELAY=-1 -spring.datasource.username=sa -spring.datasource.password= +spring.datasource.url=jdbc:mysql://3.34.144.148:3306/devcard_db?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Seoul&characterEncoding=UTF-8 +spring.datasource.username=${DB_USERNAME} +spring.datasource.password=${DB_PASSWORD} +spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver # GitHub OAuth2 -#spring.security.oauth2.client.registration.github.client-id=${GITHUB_CLIENT_ID}Ov23liJ4RxyPFx2PFFr2 -spring.security.oauth2.client.registration.github.client-id=Ov23liJ4RxyPFx2PFFr2 -#spring.security.oauth2.client.registration.github.client-secret=${GITHUB_CLIENT_SECRET} -spring.security.oauth2.client.registration.github.client-secret=242119bd6eab9242959ac61f3d0af7a4a2ce75a8 -#spring.security.oauth2.client.registration.github.redirect-uri=${GITHUB_REDIRECT_URI} -spring.security.oauth2.client.registration.github.redirect-uri=http://localhost:8080/login/oauth2/code/github -spring.security.oauth2.client.registration.github.scope=read:user +spring.security.oauth2.client.registration.github.client-id=${GH_CLIENT_ID} +spring.security.oauth2.client.registration.github.client-secret=${GH_CLIENT_SECRET} +spring.security.oauth2.client.registration.github.redirect-uri=${GH_REDIRECT_URI} +spring.security.oauth2.client.registration.github.scope=user:email spring.security.oauth2.client.provider.github.token-uri=https://github.com/login/oauth/access_token spring.security.oauth2.client.provider.github.user-info-uri=https://api.github.com/user spring.security.oauth2.client.registration.github.client-name=GitHub @@ -35,8 +32,11 @@ logging.level.org.springframework.security=DEBUG # QR Code Service -qr.domain.uri=http://localhost:8080/ +qr.domain.uri=http://3.34.144.148:8080/ qr.code.directory=src/main/resources/static/qrcodes/ # Kakao Service +# application-secret.properties? ????? ?? spring.config.import=optional:classpath:application-secret.properties + +logging.level.com.devcard.devcard=DEBUG diff --git a/src/main/resources/data.sql b/src/main/resources/data.sql index e80c3b1..4e1e519 100644 --- a/src/main/resources/data.sql +++ b/src/main/resources/data.sql @@ -1,9 +1,9 @@ -- 회원 관련 data.sql (line 1~6) -- 예시 회원 데이터 -INSERT INTO member (member_id, create_date, email, github_id, nickname, profile_img, role, username) VALUES - (1, CURRENT_TIMESTAMP, 'member1@example.com', 'github1', 'Member1', 'https://picsum.photos/200', 'USER', 'member1'), - (2, CURRENT_TIMESTAMP, 'member2@example.com', 'github2', 'Member2', 'https://picsum.photos/200', 'USER', 'member2'), - (3, CURRENT_TIMESTAMP, 'member3@example.com', 'github3', 'Member3', 'https://picsum.photos/200', 'USER', 'member3'); +INSERT INTO member (create_date, email, github_id, nickname, profile_img, role, username) VALUES + (CURRENT_TIMESTAMP, 'member1@example.com', 'github1', 'Member1', 'https://picsum.photos/200', 'USER', 'member1'), + (CURRENT_TIMESTAMP, 'member2@example.com', 'github2', 'Member2', 'https://picsum.photos/200', 'USER', 'member2'), + (CURRENT_TIMESTAMP, 'member3@example.com', 'github3', 'Member3', 'https://picsum.photos/200', 'USER', 'member3'); -- 채팅 관련 data.sql (line 8~25) -- 예시 채팅방 데이터 @@ -13,7 +13,7 @@ INSERT INTO chat_room (created_at, last_message, last_message_time) VALUES (CURRENT_TIMESTAMP, '테스트 채팅방 3', '2024-11-07 16:59:34'); -- 예시 채팅 메시지 데이터 -INSERT INTO chat_message (content, sender, timestamp, chat_room_id) VALUES +INSERT INTO chat_message (content, sender, `timestamp`, chat_room_id) VALUES ('테스트 메시지 1', 'user_1', '2023-11-07 16:59:34', 1), ('테스트 메시지 2', 'user_2', '2024-10-07 16:59:34', 2), ('테스트 메시지 3', 'user_3', '2024-11-07 16:59:34', 3); @@ -34,14 +34,14 @@ INSERT INTO card (member_id, company, position, phone, bio) VALUES (2, 'kakaoCompany', '백엔드 주니어 개발자', '010-7894-456', '소통하는 개발자입니다.'); -- Group 데이터 -INSERT INTO groups (name, member_id) VALUES - ('친구', 1), - ('회사', 1), - ('동아리', 1); +INSERT INTO `groups` (`name`, member_id) VALUES + ('친구', 1), + ('회사', 1), + ('동아리', 1); INSERT INTO group_card (group_id, card_id) VALUES (1, 1), (1, 2), (2, 2), (3, 1), - (3, 2); + (3, 2); \ No newline at end of file diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql index 210de3b..8fb6bbc 100644 --- a/src/main/resources/schema.sql +++ b/src/main/resources/schema.sql @@ -1,32 +1,24 @@ -- 채팅 관련 schema.sql (line 1~39) --- 회원(MEMBERS) 스키마 --- CREATE TABLE IF NOT EXISTS chat_user ( --- id BIGINT PRIMARY KEY AUTO_INCREMENT, --- name VARCHAR(255), --- company VARCHAR(255), --- position VARCHAR(255), --- email VARCHAR(255), --- phone VARCHAR(255), --- timestamp TIMESTAMP --- ); -- 채팅방(CHAT ROOM) 스키마 CREATE TABLE IF NOT EXISTS chat_room ( - id BIGINT PRIMARY KEY AUTO_INCREMENT, - created_at TIMESTAMP, + id BIGINT AUTO_INCREMENT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, last_message VARCHAR(255), - last_message_time TIMESTAMP -); + last_message_time TIMESTAMP, + PRIMARY KEY (id) + ); -- 채팅 메시지(CHAT MESSAGE) 스키마 CREATE TABLE IF NOT EXISTS chat_message ( - id BIGINT PRIMARY KEY AUTO_INCREMENT, + id BIGINT AUTO_INCREMENT, content VARCHAR(2000), sender VARCHAR(255), - timestamp TIMESTAMP, + `timestamp` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, chat_room_id BIGINT, + PRIMARY KEY (id), CONSTRAINT fk_chat_room FOREIGN KEY (chat_room_id) REFERENCES chat_room(id) -); + ); -- 채팅방 참가자(CHAT ROOM PARTICIPANTS) 스키마 CREATE TABLE IF NOT EXISTS chat_room_participants ( @@ -35,19 +27,51 @@ CREATE TABLE IF NOT EXISTS chat_room_participants ( PRIMARY KEY (chat_room_id, participants_id), CONSTRAINT fk_participants_user FOREIGN KEY (participants_id) REFERENCES member(id), CONSTRAINT fk_participants_chat_room FOREIGN KEY (chat_room_id) REFERENCES chat_room(id) -); + ); -- 명함(Card) 테이블 스키마 CREATE TABLE IF NOT EXISTS card ( - id BIGINT PRIMARY KEY AUTO_INCREMENT, --- github_id VARCHAR(255), --- name VARCHAR(255), + id BIGINT AUTO_INCREMENT, + member_id BIGINT, company VARCHAR(255), position VARCHAR(255), --- email VARCHAR(255), phone VARCHAR(255), --- profile_picture VARCHAR(255), bio VARCHAR(500), created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (id), + CONSTRAINT fk_card_member FOREIGN KEY (member_id) REFERENCES member(member_id) + ); + + +-- 회원(Member) 테이블 스키마 +CREATE TABLE IF NOT EXISTS member ( + member_id BIGINT AUTO_INCREMENT, + create_date TIMESTAMP, + email VARCHAR(255), + github_id VARCHAR(255), + nickname VARCHAR(255), + profile_img VARCHAR(255), + role VARCHAR(255), + username VARCHAR(255), + PRIMARY KEY (member_id) + ); + + +-- 그룹(Groups) 테이블 스키마 +CREATE TABLE IF NOT EXISTS `groups` ( + id BIGINT AUTO_INCREMENT, + `name` VARCHAR(255), + member_id BIGINT, + PRIMARY KEY (id), + CONSTRAINT fk_group_member FOREIGN KEY (member_id) REFERENCES member(member_id) + ); + +-- 그룹 카드(Group_Card) 테이블 스키마 +CREATE TABLE IF NOT EXISTS group_card ( + group_id BIGINT, + card_id BIGINT, + PRIMARY KEY (group_id, card_id), + CONSTRAINT fk_group_card_group FOREIGN KEY (group_id) REFERENCES `groups`(id), + CONSTRAINT fk_group_card_card FOREIGN KEY (card_id) REFERENCES card(id) ); \ No newline at end of file diff --git a/src/main/resources/static/css/card/card-create.css b/src/main/resources/static/css/card/card-create.css new file mode 100644 index 0000000..56dd39f --- /dev/null +++ b/src/main/resources/static/css/card/card-create.css @@ -0,0 +1,72 @@ +body, html { + margin: 0; + padding: 0; + height: 100%; + overflow: hidden; +} + +.header { + display: flex; + justify-content: center; + align-items: center; + padding: 1rem; + margin: 1rem 0 0.5rem 0; + font-size: 0.8rem; +} + +.scrollable-container { + height: calc(100% - 80px); /* 헤더와 네비게이션 바를 제외한 높이 조정 */ + overflow-y: auto; +} + +.card-create-container { + width: 90%; + max-width: 500px; + margin: 0 auto; + padding: 20px; + border: 1px solid #ccc; + border-radius: 8px; + background-color: #f9f9f9; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); +} + +/* 폼 그룹 스타일 */ +.form-group { + margin-bottom: 15px; +} + +.form-group label { + display: block; + font-weight: bold; + margin-bottom: 5px; + color: #555; +} + +/* 입력 필드 스타일 */ +.form-group input, +.form-group textarea { + width: 100%; + padding: 8px; + border: 1px solid #ccc; + border-radius: 4px; +} + +.form-group textarea { + resize: vertical; +} + +/* 제출 버튼 스타일 */ +.btn-submit { + width: 100%; + padding: 10px; + background-color: var(--cnu-sky-blue); + color: white; + border: none; + border-radius: 4px; + font-size: 16px; + cursor: pointer; +} + +.btn-submit:hover { + background-color: #0056b3; +} diff --git a/src/main/resources/static/css/card/card-detail.css b/src/main/resources/static/css/card/card-detail.css index c02c631..4338987 100644 --- a/src/main/resources/static/css/card/card-detail.css +++ b/src/main/resources/static/css/card/card-detail.css @@ -1,83 +1,198 @@ -body { + +/* Header styling (like home.css) */ +.header { + display: flex; + justify-content: center; + align-items: center; + padding: 1rem; /* Increased padding */ + margin: 0.5rem 0; + color: black; font-family: Arial, sans-serif; - margin: 0; - padding: 0; - background-color: #f5f5f5; } +.title { + text-align: center; + font-size: 1.5rem; /* Increased font size for better emphasis */ + margin-bottom: 20px; +} + +/* Container for the main content */ .container { + flex-grow: 1; display: flex; - flex-direction: column; - align-items: center; justify-content: center; - min-height: 80vh; + align-items: center; padding: 20px; + flex-flow: column; +} + +/* Share button styling */ +.share-button { + margin-top: 20px; + padding: 10px 20px; /* More compact padding */ + border-radius: 50px; + background-color: var(--cnu-dark-blue); /* Bright yellow button */ + color: white; + border: none; + font-size: 1rem; + cursor: pointer; + box-shadow: 0 6px 14px rgba(0, 0, 0, 0.2); + transition: background-color 0.3s ease, transform 0.2s ease; + text-transform: uppercase; + width: 100%; + max-width: 220px; + display: flex; + justify-content: center; + align-items: center; +} + +/* Hover effect for share button */ +.share-button:hover { + background-color: var(--cnu-yellow); + transform: scale(1.1); +} + +.share-button:active { + transform: scale(1); +} + +/* Responsive design for smaller screens */ +@media (max-width: 480px) { + .card { + width: 95%; + padding: 20px; + } + + .card h3 { + font-size: 1.3rem; + } + + .card .github-link { + font-size: 0.9em; + } + + .share-button { + padding: 12px 24px; + font-size: 1.1em; + } } -.card { - background-color: #000; - color: #fff; +#modal-overlay { + position: fixed; + top: 0; + left: 0; + width: 100dvw; + height: 100dvh; + background-color: rgba(0, 0, 0, 0.3); + z-index: 1000; /* 배경이 페이지 전체를 덮도록 설정 */ + display: none; /* 기본적으로 숨김 */ +} + +/* 모달 창 설정 */ +#group-modal { + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + background-color: rgb(255 255 255 / 94%); padding: 20px; - border-radius: 10px; - width: 80%; - max-width: 400px; + width: 300px; + box-shadow: 0px 4px 12px rgba(0, 0, 0, 0.15); + border-radius: 8px; + z-index: 1001; /* 모달 창이 배경 위에 표시되도록 설정 */ + display: none; /* 기본적으로 숨김 */ +} + +/* 모달과 배경이 활성화될 때 */ +#modal-overlay.visible, +#group-modal.visible { + display: block; +} + +.group-option { + display: block; + width: 100%; + padding: 10px; + font-size: 16px; + color: #333; text-align: center; - box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); + border: none; + background: none; + cursor: pointer; + transition: all 0.3s ease; } -.card h2 { - font-size: 1.5em; - margin-bottom: 10px; +.group-option:hover { + color: #007bff; + transform: translateY(-2px); } -.github-link { - color: #00aced; - text-decoration: none; +#group-list { + list-style: none; + padding: 0; + margin: 0; } -.github-link:hover { - text-decoration: underline; +#group-modal h3 { + font-size: 20px; + margin-bottom: 15px; + text-align: center; + color: #555; } -.share-button { - margin-top: 20px; - padding: 15px; - border-radius: 50%; - background-color: #3b5998; /* Kakao 버튼 색상 */ - color: #fff; +.add-to-group-button { + display: block; + margin: 10px auto; + padding: 10px 20px; + font-size: 16px; + color: #ffffff; + background-color: var(--cnu-yellow); border: none; - font-size: 1em; + border-radius: 20px; cursor: pointer; - box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); + transition: background-color 0.3s ease; } -.share-button:hover { - background-color: #2a427a; +.add-to-group-button:hover { + background-color: var(--cnu-orange); } -.nav-bar { - display: flex; - justify-content: space-around; - width: 100%; - position: fixed; - bottom: 0; - background-color: #fff; - box-shadow: 0 -1px 5px rgba(0, 0, 0, 0.1); - padding: 10px 0; -} -.nav-item { - display: flex; - flex-direction: column; - align-items: center; - font-size: 0.9em; +/* Base button styling */ +.button { + padding: 10px 20px; + font-size: 16px; + border: none; + border-radius: 5px; cursor: pointer; + transition: background-color 0.3s; } -.nav-item p { - margin-top: 5px; +/* Edit button styling */ +.edit-button.button { + background-color: grey; /* Green */ + color: white; } -.nav-icon { - width: 24px; +.edit-button.button:hover { + background-color: #16a085; /* Darker green */ +} + +/* Delete button styling */ +.delete-button.button { + background-color: grey; /* Red */ + color: white; + margin-left: 10px; /* Spacing between buttons */ +} + +.delete-button.button:hover { + background-color: #16a085; /* Darker red */ +} + +#qr-image-container { + display: none; + text-align: center; + margin-top: 10px; /* 기존 20px에서 10px로 줄임 */ + position: relative; /* 컨테이너의 위치를 세부 조정할 수 있도록 설정 */ + top: -10px; /* 컨테이너를 위로 이동 */ } diff --git a/src/main/resources/static/css/card/card-list.css b/src/main/resources/static/css/card/card-list.css new file mode 100644 index 0000000..4f8104b --- /dev/null +++ b/src/main/resources/static/css/card/card-list.css @@ -0,0 +1,58 @@ +/* 전체 화면 설정 */ +body, html { + margin: 0; + padding: 0; + height: 100%; + display: flex; + flex-direction: column; +} + +/* 컨테이너 설정 */ +.container { + flex: 1; + display: flex; + flex-direction: column; + align-items: center; + padding: 20px; + overflow-y: auto; /* 스크롤 가능하게 설정 */ +} + +/* 카드 리스트 가운데 정렬 */ +.card-list { + display: flex; + flex-direction: column; + align-items: center; + width: 100%; + max-width: 600px; /* 카드 리스트의 최대 너비 설정 */ +} + +/* 상단 헤더 */ +.header { + display: flex; + justify-content: center; + align-items: center; + padding: 1rem; + margin: 1rem 0 0.5rem 0; + font-size: 1.2rem; +} + +/* 삭제 버튼 */ +.delete-button { + display: none; + position: absolute; + top: 10px; + right: 10px; + background-color: red; + color: white; + border: none; + border-radius: 5px; + padding: 5px 10px; + cursor: pointer; + font-size: 0.8rem; + z-index: 10; /* 삭제 버튼이 다른 요소 위에 표시되도록 설정 */ +} + +/* 카드 hover 시 삭제 버튼 표시 */ +.card:hover .delete-button { + display: block; +} diff --git a/src/main/resources/static/css/card/card-manage.css b/src/main/resources/static/css/card/card-manage.css new file mode 100644 index 0000000..abb8486 --- /dev/null +++ b/src/main/resources/static/css/card/card-manage.css @@ -0,0 +1,61 @@ +.container { + height: 100dvh; + overflow: hidden; + display: flex; + flex-direction: column; +} + +.header { + display: flex; + justify-content: center; + align-items: center; + padding: 1rem; + margin: 1rem 0 0.5rem 0; + font-size: 0.8rem; +} + +/* 명함 추가 버튼 위치 */ +.add-card-btn-container { + position: absolute; /* 절대 위치 지정 */ + right: 30px; + top: 130px; + margin-bottom: 1rem; /* 아래쪽 여백 추가 */ + width: fit-content; /* 버튼 크기만큼 영역 차지 */ +} + +/* 명함 추가 버튼 스타일 */ +.add-card-button { + padding: 0.5rem 1.2rem; + background-color: var(--cnu-sky-blue); + border: none; + border-radius: 5px; + color: white; + cursor: pointer; + font-weight: bold; + transition: background-color 0.3s; +} + +.add-card-button:hover { + background-color: var(--cnu-orange); +} + +/* 명함 리스트 섹션 */ +.card-list-section { + height: 100%; + overflow-y: auto; + padding: 1rem; + display: grid; + grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); + gap: 1rem; +} + +/* 반응형 디자인 */ +@media (max-width: 768px) { + .add-card-btn-container { + justify-content: center; /* 작은 화면에서는 중앙 정렬 */ + padding-left: 0; + } + .card-list-section { + grid-template-columns: 1fr; + } +} diff --git a/src/main/resources/static/css/card/card.css b/src/main/resources/static/css/card/card.css new file mode 100644 index 0000000..c273abe --- /dev/null +++ b/src/main/resources/static/css/card/card.css @@ -0,0 +1,108 @@ +/* Styling the business card */ +.card { + display: flex; + align-items: center; + justify-content: space-between; + flex-direction: row-reverse; + background-color: #2c3e50; /* Dark blue-gray background */ + color: #ecf0f1; /* Light text color */ + border-radius: 15px; + padding: 20px; + margin: 10px auto; /* Small margin around the card */ + width: 95%; /* Nearly full width of the screen */ + max-width: 500px; + box-shadow: 0 6px 10px rgba(0, 0, 0, 0.15); + transition: transform 0.2s ease, box-shadow 0.2s ease; + cursor: pointer; + word-wrap: break-word; + overflow: hidden; +} + +.card:hover { + transform: scale(1.02); + box-shadow: 0 8px 15px rgba(0, 0, 0, 0.3); +} + +.card-image img { + width: 80px; + height: 80px; + border-radius: 50%; + object-fit: cover; + margin: 0 15px 0 0; +} + +.card-content { + flex: 1; + padding-right: 10px; +} + +.card-content h3 { + font-size: 1.4rem; + margin-bottom: 8px; + color: #ffffff; + word-break: break-word; +} + +.card-content p { + margin-bottom: 6px; + display: flex; + align-items: center; + font-size: 0.9rem; + line-height: 1.2; + overflow-wrap: break-word; + word-wrap: break-word; + word-break: break-word; +} + +.card-content p::before { + content: '•'; + color: #3498db; + margin-right: 8px; + font-size: 1.2rem; +} + +.card-content a { + color: #1abc9c; + text-decoration: none; + font-weight: bold; + word-break: break-word; +} + +.card-content a:hover { + color: #16a085; + text-decoration: underline; +} + +/* Responsive styling for smaller screens */ +@media (max-width: 600px) { + .card { + width: 95%; + padding: 15px; + } + + .card-image img { + width: 70px; + height: 70px; + margin: 0 auto 15px auto; + } + + .card-content { + padding: 0; + } + + .card-content h3 { + font-size: 1.2rem; + text-align: center; + } + + .card-content p { + font-size: 0.9rem; + text-align: center; + } + + .card-content a { + display: block; + word-break: break-word; + text-align: center; + } +} diff --git a/src/main/resources/static/css/card/home-nav-bar.css b/src/main/resources/static/css/card/home-nav-bar.css new file mode 100644 index 0000000..98fca2d --- /dev/null +++ b/src/main/resources/static/css/card/home-nav-bar.css @@ -0,0 +1,93 @@ +/* home-nav-bar.css*/ +.nav-bar { + display: flex; + justify-content: space-around; + background-color: white; + padding: 10px 0; + box-shadow: 0 -2px 5px rgba(0, 0, 0, 0.1); + position: relative; /* 자식 요소의 절대 위치 지정 가능하게 설정 */ +} + +#share-button { + position: absolute; + bottom: 40px; /* nav-bar 내에서의 위치를 조정 */ + left: 50%; /* 가운데 정렬 */ + transform: translateX(-50%); /* 가운데 정렬 보정 */ + width: 70px; + height: 70px; + background-color:#D4D6DD; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + box-shadow: 0 4px 10px rgba(0, 0, 0, 0.8); + overflow: hidden; + z-index: 10; /* 다른 요소와 겹치지 않도록 설정 */ +} + +#share-button img { + width: 40px; + height: 40px; + margin: 0; + display: block; +} + +#share-button p { + display: none; +} + +#share-options { + display: none; + position: absolute; + bottom: 110px; /* nav-bar 바로 위에 위치하도록 설정 */ + left: 50%; + transform: translateX(-50%); + flex-direction: row; + gap: 10px; + align-items: center; + justify-content: center; + z-index: 10; /* nav-bar와 겹치지 않도록 설정 */ +} + +/* 공유 버튼 3개 아이콘 */ +.share-button-icon { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + width: 80px; /* 동일한 너비 설정 */ + height: 80px; /* 동일한 높이 설정 */ + font-size: 0.9rem; + font-weight: bold; + color: white; + background-color: var(--cnu-dark-blue); + border: none; + border-radius: 30%; + cursor: pointer; + transition: background-color 0.3s, transform 0.2s; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); +} + +#kakao-share-btn, #nfc-share-btn { + margin-top: 70px; /* 카카오톡, nfc 버튼을 약간 아래로 */ +} + +/* Icon style within the share button */ +.share-button-icon img.icon { + width: 40px; + height: 40px; + margin-bottom: 4px; /* 아이콘과 텍스트 사이 여백 */ +} + +/* Hover effect for share buttons */ +.share-button-icon:hover { + background-color: var(--cnu-yellow); + transform: translateY(-3px); +} + +/* Active state for share buttons */ +.share-button-icon:active { + background-color: var(--cnu-sky-blue); + transform: translateY(1px); + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); +} diff --git a/src/main/resources/static/css/card/wallet-list.css b/src/main/resources/static/css/card/wallet-list.css index fc6e9a4..bacd4ec 100644 --- a/src/main/resources/static/css/card/wallet-list.css +++ b/src/main/resources/static/css/card/wallet-list.css @@ -87,14 +87,58 @@ margin-right: 15px; } -.group-name { +.group-name, .group-name-input { flex-grow: 1; font-size: 1rem; font-weight: bold; color: #333; } +.group-name-input { + padding: 5px; +} + .group-count { font-size: 0.9rem; color: #666; +} + +.edit-button, .delete-button { + display: none; + margin-left: 10px; + padding: 5px 10px; + border: none; + border-radius: 7px; + cursor: pointer; + transition: background-color 0.3s ease; +} + +.edit-button { + background-color: var(--cnu-light-blue); + color: white; +} + +.delete-button { + background-color: var(--pantone-877c); + color: var(--pantone-blackc); +} + +.group-item:hover .edit-button { + display: inline-block; +} + +.group-item:hover .delete-button { + display: inline-block; +} + +.edit-button:hover { + background-color: var(--cnu-dark-blue); +} + +.edit-button.editing { + background-color: var(--cnu-yellow); +} + +.delete-button:hover { + background-color: var(--pantone-871c); } \ No newline at end of file diff --git a/src/main/resources/static/css/chat/room.css b/src/main/resources/static/css/chat/room.css index 9067835..baa8884 100644 --- a/src/main/resources/static/css/chat/room.css +++ b/src/main/resources/static/css/chat/room.css @@ -1,12 +1,5 @@ -div.header > span { - cursor: pointer; - color: var(--cnu-light-blue); - position: absolute; - left: 2rem; -} - .container { - height: 100vh; + height: 100dvh; overflow: hidden; display: flex; flex-direction: column; diff --git a/src/main/resources/static/css/global.css b/src/main/resources/static/css/global.css index b8b6f48..7ff7567 100644 --- a/src/main/resources/static/css/global.css +++ b/src/main/resources/static/css/global.css @@ -27,7 +27,7 @@ body { display: flex; flex-direction: column; justify-content: space-between; - height: 100vh; + height: 100dvh; background-color: var(--background); } @@ -69,3 +69,50 @@ body { .nav-item.active p { color: var(--cnu-light-blue); } + +/* 네비게이션 바 고정 */ +.nav-bar-fixed { + position: fixed; + bottom: 0; + width: 100%; + height: 60px; /* 네비게이션 바 높이 설정 */ + z-index: 1000; +} + +/* Toast Notification CSS */ +.toast { + visibility: hidden; + min-width: 90%; + background-color: var(--pantone-877c); /* 기본 배경 색상 */ + color: #fff; + text-align: center; + border-radius: 8px; + padding: 16px; + position: fixed; + z-index: 999; + left: 50%; + bottom: 13%; + transform: translateX(-50%); + font-size: 15px; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); +} + +.toast.show { + visibility: visible; +} + +.toast.error { + background-color: var(--cnu-orange); /* 오류 알림 배경 색상 */ +} + +.toast.success { + background-color: var(--cnu-sky-blue); /* 정상 동작 알림 배경 색상 */ +} + +/* Back button CSS */ +div.header > span#backButton { + cursor: pointer; + color: var(--cnu-light-blue); + position: absolute; + left: 2rem; +} diff --git a/src/main/resources/static/css/home.css b/src/main/resources/static/css/home.css index c262065..493b15f 100644 --- a/src/main/resources/static/css/home.css +++ b/src/main/resources/static/css/home.css @@ -1,67 +1,13 @@ -/* General styles */ -* { - margin: 0; - padding: 0; - box-sizing: border-box; - font-family: Arial, sans-serif; -} - -body { - display: flex; - flex-direction: column; - justify-content: space-between; - height: 100vh; - background-color: #f5f5f5; -} - -/* Container for the main content */ -.container { - flex-grow: 1; - display: flex; - justify-content: center; - align-items: center; - padding: 20px; -} - -.title { - text-align: center; - margin-bottom: 20px; -} - -/* Styling the business card */ -.card { - display: flex; - align-items: center; - justify-content: space-between; - background-color: black; - color: white; - border-radius: 15px; - padding: 20px; - margin: 10px; - width: 90%; - max-width: 500px; - box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); -} - -.card-content { - flex: 1; -} - -.card-content h3 { - font-size: 1.5rem; - margin-bottom: 10px; -} - -.card-content p { - margin-bottom: 8px; -} - -.card-content a { - color: #00bfff; - text-decoration: none; -} - -.card-image img { - width: 100px; - height: auto; -} +.card-title-container { + flex-grow: 1; + display: flex; + justify-content: center; + align-items: center; + padding: 20px; + flex-flow: column; +} + +.title { + text-align: center; + margin-bottom: 20px; +} diff --git a/src/main/resources/static/css/login.css b/src/main/resources/static/css/login.css deleted file mode 100644 index dcae3b2..0000000 --- a/src/main/resources/static/css/login.css +++ /dev/null @@ -1,90 +0,0 @@ -/* General styles */ -* { - margin: 0; - padding: 0; - box-sizing: border-box; - font-family: Arial, sans-serif; -} - -body { - display: flex; - justify-content: center; - align-items: center; - height: 100vh; - background-color: #f5f5f5; -} - -/* Container for the login card */ -.login-container { - text-align: center; - padding: 20px; - background-color: white; - border-radius: 10px; - box-shadow: 0px 4px 12px rgba(0, 0, 0, 0.1); - width: 320px; -} - -/* Circle logo */ -.logo-circle { - width: 80px; - height: 80px; - background-color: #4285f4; - border-radius: 50%; - display: flex; - justify-content: center; - align-items: center; - margin: 0 auto; -} - -.logo-letter { - color: white; - font-size: 36px; - font-weight: bold; -} - -.app-name { - margin-top: 20px; - font-size: 24px; - font-weight: bold; - color: #333; -} - -/* GitHub login button */ -.github-login-btn { - display: flex; - align-items: center; - justify-content: center; - background-color: black; - color: white; - border: none; - border-radius: 30px; - padding: 12px 20px; - font-size: 16px; - margin: 20px auto; - cursor: pointer; - width: 100%; - max-width: 280px; - transition: background-color 0.3s; -} - -.github-login-btn:hover { - background-color: #333; -} - -.github-login-btn img { - width: 24px; - height: 24px; - margin-right: 10px; -} - -/* Terms and conditions */ -.terms { - font-size: 12px; - color: #666; - margin-top: 20px; -} - -.terms a { - color: #4285f4; - text-decoration: none; -} diff --git a/src/main/resources/static/css/mypage.css b/src/main/resources/static/css/mypage.css new file mode 100644 index 0000000..7335bf7 --- /dev/null +++ b/src/main/resources/static/css/mypage.css @@ -0,0 +1,74 @@ +/* Mypage Specific Styles */ +:root { + --profile-text-color: #555; +} + +.profile-section { + display: flex; + align-items: center; + margin-left: 20px; + padding: 20px; + border-bottom: 1px solid #ddd; +} + +.profile-photo img { + width: 100px; + height: 100px; + border-radius: 50%; + object-fit: cover; + margin-right: 20px; +} + +.profile-info .username { + padding: 50px; + font-size: 1.5rem; + color: var(--profile-text-color); +} + +.menu-section { + display: flex; + flex-direction: column; +} + +.menu-item { + padding: 0; + margin: 2px; + border-bottom: 1px solid #ddd; + cursor: pointer; + transition: background-color 0.3s; +} + +.menu-item:last-child { + border-bottom: none; +} + + +.menu-item a { + display: flex; + align-items: center; + padding: 20px; + width: 100%; + height: 100%; + text-decoration: none; + color: inherit; +} + +.menu-item img { + width: 32px; + height: 32px; + margin-right: 20px; +} + +.menu-item p { + flex-grow: 1; + font-size: 1rem; + font-weight: bold; + color: #333; +} + +.menu-item:hover { + background-color: var(--cnu-light-blue); + color: white; + transition: color 0.3s; +} + diff --git a/src/main/resources/static/css/mypage/detail-style.css b/src/main/resources/static/css/mypage/detail-style.css new file mode 100644 index 0000000..6e8f7ba --- /dev/null +++ b/src/main/resources/static/css/mypage/detail-style.css @@ -0,0 +1,69 @@ +:root { + --cnu-light-blue: #3f8efc; + --text-color-dark: #333; + --text-color-light: #888; +} + +body { + margin: 0; + font-family: Arial, sans-serif; + color: var(--text-color-dark); + background-color: #f9f9f9; +} + +/* 헤더 스타일 */ +.header { + display: flex; + align-items: center; + font-size: 1.5rem; + padding: 1rem; + color: white; + background-color: var(--cnu-light-blue); + position: relative; + gap: 0.5rem; /* 아이콘과 제목 간격 추가 */ +} + +.header #backButton { + position: relative; + left: 0; + cursor: pointer; + font-size: 1.5rem; +} + +.header .title { + margin-left: 1.5rem; /* 화살표와 제목 사이 간격 추가 */ +} + + +/* 공지사항 세부 내용 스타일 */ +.notice-detail { + padding: 2rem 1.5rem; + background-color: white; + border-radius: 8px; + margin: 1rem; + box-shadow: 0px 4px 8px rgba(0, 0, 0, 0.1); +} + +.notice-detail h2 { + font-size: 1.8rem; + margin-bottom: 0.3rem; +} + +.notice-detail p#detail-date { + font-size: 0.9rem; + color: var(--text-color-light); + margin-bottom: 1.2rem; +} + +.notice-detail #detail-content { + font-size: 1rem; + line-height: 1.6; + padding-left: 0.5rem; +} + +/* 공지사항 날짜 스타일 */ +.detail-date { + font-size: 0.9rem; + color: #888; + margin-left: auto; /* 날짜를 오른쪽으로 정렬 */ +} diff --git a/src/main/resources/static/css/mypage/list-style.css b/src/main/resources/static/css/mypage/list-style.css new file mode 100644 index 0000000..bddef07 --- /dev/null +++ b/src/main/resources/static/css/mypage/list-style.css @@ -0,0 +1,70 @@ +/* 전체 배경 스타일 */ +body { + font-family: Arial, sans-serif; + margin: 0; + padding: 0; + background-color: #f5f5f5; +} + +/* 헤더 스타일 */ +.header { + display: flex; + align-items: center; + padding: 10px; + background-color: #3f8efc; + color: white; +} + +/* 뒤로가기 버튼 스타일 */ +#backButton { + font-size: 24px; + cursor: pointer; /* 마우스를 올렸을 때 포인터 커서 */ + margin-right: 10px; /* 오른쪽 여백 */ + margin-top: 5px; /* 위쪽 여백 */ +} + +/* 제목 스타일 */ +.title { + font-size: 1.5rem; + font-weight: bold; + flex-grow: 1; + text-align: center; +} + +/* 공지사항 목록 스타일 */ +.notice-list { + padding: 10px; +} + +/* 공지사항 항목 스타일 */ +.notice-item { + list-style: none; + padding: 15px; /* 상하좌우 동일한 15px 여백 */ + margin: 10px 0; + background-color: white; + border-radius: 5px; + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); + display: flex; + align-items: center; + transition: background-color 0.2s; +} + +/* 공지사항 제목 스타일 */ +.notice-title { + font-size: 1.1rem; + font-weight: bold; + margin-left: 10px; /* 제목을 왼쪽에서 약간 띄우기 */ + color: #3f8efc; +} + +/* 공지사항 날짜 스타일 */ +.notice-date { + font-size: 0.9rem; + color: #888; + margin-left: auto; /* 날짜를 오른쪽으로 정렬 */ +} + +/* 공지사항 항목 호버 스타일 */ +.notice-item:hover { + background-color: #eaeaea; +} diff --git a/src/main/resources/static/css/mypage/qna-common.css b/src/main/resources/static/css/mypage/qna-common.css new file mode 100644 index 0000000..24097cc --- /dev/null +++ b/src/main/resources/static/css/mypage/qna-common.css @@ -0,0 +1,35 @@ +:root { + --cnu-light-blue: #3f8efc; + --text-color-dark: #333; + --text-color-light: #888; + --background-color: #f9f9f9; + --border-radius: 8px; + --box-shadow: 0px 4px 8px rgba(0, 0, 0, 0.1); +} + +body { + margin: 0; + font-family: Arial, sans-serif; + color: var(--text-color-dark); + background-color: var(--background-color); +} + +/* 공통 헤더 스타일 */ +.header { + display: flex; + align-items: center; + font-size: 1.5rem; + padding: 1rem; + color: white; + background-color: var(--cnu-light-blue); + gap: 0.5rem; +} + +.header #backButton { + cursor: pointer; + font-size: 1.5rem; +} + +.header .title { + margin-left: 1.5rem; +} diff --git a/src/main/resources/static/css/mypage/qna-create.css b/src/main/resources/static/css/mypage/qna-create.css new file mode 100644 index 0000000..c63a874 --- /dev/null +++ b/src/main/resources/static/css/mypage/qna-create.css @@ -0,0 +1,107 @@ +/* 전체 배경 색상 */ +html, body { + background-color: white; + font-family: Arial, sans-serif; + margin: 0; /* 상하 여백 제거 */ + padding: 0; /* 상하 여백 제거 */ + width: 100%; /* 화면 전체 너비 사용 */ + height: 100%; /* 화면 전체 높이 사용 */ + box-sizing: border-box; /* padding과 border가 전체 크기 안에 포함되도록 설정 */ +} + +/* QnA 상세 헤더 */ +.header { + background-color: #3f8efc; + color: white; + display: flex; + align-items: center; + padding: 10px 15px; /* 헤더 영역의 상하 여백 줄임 */ + width: 100%; /* 화면 너비 꽉 채우기 */ + box-sizing: border-box; +} + +/* 뒤로 가기 버튼 */ +#backButton { + font-size: 24px; + cursor: pointer; + margin-right: 10px; +} + +/* 제목 중앙 정렬 */ +.title { + font-size: 1.5rem; + flex-grow: 1; + text-align: center; + margin: 0; /* 제목과 상단 바 간격을 없앰 */ + padding: 0; /* 기본 여백 제거 */ +} + +/* QnA 작성 폼 */ +.qna-create-container { + width: 100%; /* 화면 너비 100% 사용 */ + max-width: 100%; /* 최대 너비 제한 해제 */ + margin: 0 auto; /* 자동 여백으로 화면 가운데 배치 */ + padding: 1rem; /* 충분한 여백을 주어 화면이 꽉 차게 */ + box-sizing: border-box; + display: flex; + flex-direction: column; + justify-content: space-between; + margin-top: 0; /* 헤더와의 간격을 없앰 */ +} + +/* 폼 그룹 */ +.form-group { + margin-bottom: 1.5rem; /* 각 입력 항목 사이 여백 증가 */ +} + +label { + font-size: 1.1rem; + font-weight: bold; + display: block; + margin-bottom: 0.5rem; +} + +/* 입력 필드 */ +input[type="text"], textarea { + width: 100%; + padding: 12px; /* 입력 필드 크기 확장 */ + font-size: 1rem; + border: 1px solid #ccc; + border-radius: 4px; + box-sizing: border-box; /* 패딩과 border를 포함한 크기 계산 */ +} + +textarea { + resize: vertical; + min-height: 150px; /* 최소 높이 설정 */ +} + +/* 제출 버튼 */ +.submit-button { + background-color: #3f8efc; + color: white; + border: none; + padding: 15px 20px; /* 더 큰 버튼 크기 */ + border-radius: 5px; + cursor: pointer; + font-size: 1rem; + width: 100%; + box-sizing: border-box; /* 버튼 크기 포함 */ +} + +.submit-button:hover { + background-color: #3371d1; +} + +/* Toast 알림 */ +.toast { + position: fixed; + bottom: 20px; + left: 50%; + transform: translateX(-50%); + background-color: #333; + color: white; + padding: 10px 20px; + border-radius: 5px; + display: none; +} diff --git a/src/main/resources/static/css/mypage/qna-detail.css b/src/main/resources/static/css/mypage/qna-detail.css new file mode 100644 index 0000000..75801fa --- /dev/null +++ b/src/main/resources/static/css/mypage/qna-detail.css @@ -0,0 +1,107 @@ +/* 전체 배경 색상 */ +body { + background-color: white; + font-family: Arial, sans-serif; + margin: 0; + padding: 0; +} + +/* QnA 상세 헤더 */ +.header { + background-color: #3f8efc; + color: white; + display: flex; + align-items: center; + padding: 10px; +} + +#backButton { + font-size: 24px; + cursor: pointer; + margin-right: 10px; +} + +.title { + font-size: 1.5rem; + flex-grow: 1; + text-align: center; /* 제목만 중앙 정렬 */ +} + +/* QnA 세부 내용 */ +.qna-detail { + max-width: 800px; + margin: 1.5rem auto; + padding: 1rem; +} + +.qna-header { + margin-bottom: 1rem; +} + +#detail-title { + font-size: 1.5rem; + font-weight: bold; + text-align: left; /* 제목을 왼쪽으로 정렬 */ +} + +#detail-info { + color: #888; + font-size: 0.9rem; + text-align: left; /* 작성자, 시간도 왼쪽 정렬 */ +} + +#detail-content { + margin-bottom: 1.5rem; + font-size: 1rem; + color: #333; + text-align: left; /* 질문 내용 왼쪽 정렬 */ +} + +/* 답변 섹션 */ +.answer-section { + margin-top: 2rem; + text-align: left; /* 답변 섹션 왼쪽 정렬 */ +} + +.answer-section h3 { + font-size: 1.2rem; + color: #333; + margin-bottom: 0.5rem; +} + +#answer-content { + font-size: 1rem; + color: #555; +} + +#answer-content span { + font-style: italic; + color: #999; +} + +/* 기본 스타일을 위한 Toast 알림 */ +.toast { + position: fixed; + bottom: 20px; + left: 50%; + transform: translateX(-50%); + background-color: #333; + color: white; + padding: 10px 20px; + border-radius: 5px; + display: none; +} + +.create-button { + background-color: white; /* 버튼 배경색 */ + color: black; /* 버튼 텍스트 색상 */ + padding: 10px 20px; /* 패딩 */ + border: none; + border-radius: 5px; + cursor: pointer; + font-size: 16px; +} + +.create-button:hover { + background-color: white; /* 버튼 호버 시 색상 */ +} diff --git a/src/main/resources/static/css/mypage/qna-list.css b/src/main/resources/static/css/mypage/qna-list.css new file mode 100644 index 0000000..d601186 --- /dev/null +++ b/src/main/resources/static/css/mypage/qna-list.css @@ -0,0 +1,133 @@ +/* 전체 배경 스타일 */ +body { + font-family: Arial, sans-serif; + margin: 0; + padding: 0; + background-color: #f5f5f5; +} + +/* 헤더 스타일 */ +.header { + display: flex; + align-items: center; + padding: 10px; + background-color: #3f8efc; + color: white; +} + +/* 뒤로가기 버튼 스타일 */ +#backButton { + font-size: 24px; + cursor: pointer; + margin-right: 10px; + margin-top: 5px; +} + +/* 제목 스타일 */ +.title { + font-size: 1.5rem; + font-weight: bold; + flex-grow: 1; + text-align: center; +} + +/* Q & A 리스트 컨테이너 */ +.qna-list-container { + max-width: 800px; + margin: 1.5rem auto; + padding: 0 1rem; +} + +/* Q & A 항목 스타일 */ +.qna-item { + list-style: none; + padding: 1rem 1.5rem; + margin: 0.75rem 0; + background-color: white; + border-radius: 5px; + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); + display: flex; + align-items: center; + justify-content: space-between; /* 항목들을 양옆에 배치 */ + transition: background-color 0.2s; +} + +.qna-item:hover { + background-color: #f0f5ff; +} + +/* 각 항목 스타일: ID, 작성자, 제목, 날짜, 상태 */ +.qna-id, +.qna-name, +.qna-title, +.qna-date, +.qna-status { + font-size: 0.9rem; + color: #888; +} + +.qna-id { + font-weight: bold; + margin-right: 10px; +} + +.qna-name { + color: #3f8efc; +} + +.qna-title { + font-size: 1.1rem; + font-weight: bold; + color: #3f8efc; + margin-left: 1rem; +} + +.qna-date { + color: #888; + margin-left: auto; /* 오른쪽 끝으로 날짜 배치 */ +} + +.qna-status { + color: #888; + margin-left: 1rem; /* 상태는 제목 오른쪽에 배치 */ +} + +/* Q & A 리스트 항목에 간격 추가 */ +.qna-item > div { + margin-right: 1rem; +} + +/* Q & A 제목, 상태, 날짜 등을 정렬하는 스타일 */ +.qna-item > .qna-id { + width: 50px; /* ID의 고정 너비 */ +} + +.qna-item > .qna-name { + width: 150px; /* 작성자의 고정 너비 */ +} + +.qna-item > .qna-title { + flex-grow: 2; /* 제목은 남은 공간을 차지 */ +} + +.qna-item > .qna-date { + width: 120px; /* 날짜의 고정 너비 */ +} + +.qna-item > .qna-status { + width: 100px; /* 상태의 고정 너비 */ +} + +.create-button { + background-color: white; /* 버튼 배경색 */ + color: black; /* 버튼 텍스트 색상 */ + padding: 10px 20px; /* 패딩 */ + border: none; + border-radius: 5px; + cursor: pointer; + font-size: 16px; +} + +.create-button:hover { + background-color: white; /* 버튼 호버 시 색상 */ +} diff --git a/src/main/resources/static/css/oauth/login.css b/src/main/resources/static/css/oauth/login.css new file mode 100644 index 0000000..9851b59 --- /dev/null +++ b/src/main/resources/static/css/oauth/login.css @@ -0,0 +1,110 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +html, body { + height: 100%; + display: flex; + justify-content: center; + align-items: center; + background-color: white; + font-family: Arial, sans-serif; +} + +/* Wrapper to center content */ +.wrapper { + display: flex; + flex-direction: column; + align-items: center; + text-align: center; +} + +/* Content styles */ +.logo-circle { + width: 80px; + height: 80px; + background-color: #4285f4; +======= + font-family: Arial, sans-serif; +} + +body { + display: flex; + justify-content: center; + align-items: center; + height: 100dvh; + background-color: var(--background); +} + +/* Container for the login card */ +.wrapper { + text-align: center; + border-radius: 10px; + display: flex; + flex-direction: column; + justify-content: space-between; + padding: 1rem 2rem 3rem; +} + +/* Circle logo */ +.logo-circle { + width: 80px; + height: 80px; + background-color: var(--cnu-light-blue); + border-radius: 50%; + display: flex; + justify-content: center; + align-items: center; + margin-bottom: 5vh; /* 로고와 텍스트 사이의 간격을 vh 단위로 설정 */ +} + +.logo-letter { + color: white; + font-size: 36px; + font-weight: bold; +} + +.app-name { + font-size: 2rem; /* rem 단위를 사용해 텍스트 크기 설정 */ + font-weight: bold; + color: #333; + margin-bottom: 25vh; /* DevCard와 로그인 버튼 사이의 간격을 vh 단위로 설정 */ +} + +.github-login-btn { + display: flex; + align-items: center; + justify-content: center; + background-color: black; + color: white; + border: none; + border-radius: 30px; + padding: 12px 20px; + font-size: 1rem; + cursor: pointer; + max-width: 250px; + transition: background-color 0.3s; + margin-bottom: 5vh; /* 로그인 버튼과 약관 링크 사이의 간격을 vh 단위로 설정 */ +} + +.github-login-btn:hover { + background-color: #333; +} + +.github-icon { + filter: invert(100%); + margin-right: 10px; +} + +.terms { + font-size: 0.8rem; + color: #666; + text-align: center; +} + +.terms a { + color: #4285f4; + text-decoration: none; +} diff --git a/src/main/resources/static/css/oauth/redirect.css b/src/main/resources/static/css/oauth/redirect.css new file mode 100644 index 0000000..e69de29 diff --git a/src/main/resources/static/images/card_black.svg b/src/main/resources/static/images/card_black.svg new file mode 100644 index 0000000..2dd8c21 --- /dev/null +++ b/src/main/resources/static/images/card_black.svg @@ -0,0 +1,4 @@ + + + diff --git a/src/main/resources/static/images/customer-service.svg b/src/main/resources/static/images/customer-service.svg index 9b39afd..df6b41e 100644 --- a/src/main/resources/static/images/customer-service.svg +++ b/src/main/resources/static/images/customer-service.svg @@ -1,3 +1,4 @@ - + diff --git a/src/main/resources/static/images/kakaotalk-icon.svg b/src/main/resources/static/images/kakaotalk-icon.svg new file mode 100644 index 0000000..fab8548 --- /dev/null +++ b/src/main/resources/static/images/kakaotalk-icon.svg @@ -0,0 +1,11 @@ + + + + + + + + + + diff --git a/src/main/resources/static/images/nfc-icon.svg b/src/main/resources/static/images/nfc-icon.svg new file mode 100644 index 0000000..22ec15c --- /dev/null +++ b/src/main/resources/static/images/nfc-icon.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/main/resources/static/images/notice.svg b/src/main/resources/static/images/notice.svg index 9d92ecb..2cdf298 100644 --- a/src/main/resources/static/images/notice.svg +++ b/src/main/resources/static/images/notice.svg @@ -1,3 +1,4 @@ - + diff --git a/src/main/resources/static/images/person_black.svg b/src/main/resources/static/images/person_black.svg new file mode 100644 index 0000000..f3422a5 --- /dev/null +++ b/src/main/resources/static/images/person_black.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/main/resources/static/images/qr-icon.svg b/src/main/resources/static/images/qr-icon.svg new file mode 100644 index 0000000..97a5f47 --- /dev/null +++ b/src/main/resources/static/images/qr-icon.svg @@ -0,0 +1,4 @@ + + + diff --git a/src/main/resources/static/images/settings.svg b/src/main/resources/static/images/settings.svg index a847b39..00ff977 100644 --- a/src/main/resources/static/images/settings.svg +++ b/src/main/resources/static/images/settings.svg @@ -1,3 +1,4 @@ - + diff --git a/src/main/resources/static/images/share.svg b/src/main/resources/static/images/share.svg new file mode 100644 index 0000000..2a18460 --- /dev/null +++ b/src/main/resources/static/images/share.svg @@ -0,0 +1,4 @@ + + + diff --git a/src/main/resources/static/js/card/card-create.js b/src/main/resources/static/js/card/card-create.js new file mode 100644 index 0000000..d54f7a8 --- /dev/null +++ b/src/main/resources/static/js/card/card-create.js @@ -0,0 +1,53 @@ +// card-create.js +document.addEventListener("DOMContentLoaded", function () { + const form = document.getElementById("card-create-form"); + if (form) { + form.addEventListener("submit", function (e) { + e.preventDefault(); + + const data = { + company: document.getElementById("company").value, + position: document.getElementById("position").value, + phone: document.getElementById("phone").value || null, + bio: document.getElementById("bio").value || null, + email: document.getElementById("email").value || null, + cardName: document.getElementById("cardName").value || null, + profileImg: document.getElementById("profileImg").value || null, + }; + + const submitButton = document.querySelector(".btn-submit"); + submitButton.disabled = true; + + fetch("/cards", { + method: "POST", + headers: { + "Content-Type": "application/json" + }, + body: JSON.stringify(data) + }) + .then(response => { + if (response.ok) { + handleSuccess("명함이 성공적으로 생성되었습니다!", 3000); + setTimeout(() => { + window.location.href = "/cards/manage"; + }, 300); + } else if (response.status === 401) { + handleError("인증이 필요합니다. 로그인 후 다시 시도해주세요.", 3000); + } else { + return response.json().then(err => { + handleError(`명함 생성에 실패했습니다: ${err.message || "알 수 없는 에러입니다."}`, 3000); + }); + } + }) + .catch(error => { + console.error("Error:", error); + handleError("서버 오류로 인해 명함을 생성할 수 없습니다.", 3000); + }) + .finally(() => { + submitButton.disabled = false; + }); + }); + } else { + console.error("Form with ID 'card-create-form' not found"); + } +}); diff --git a/src/main/resources/static/js/card/card-detail.js b/src/main/resources/static/js/card/card-detail.js new file mode 100644 index 0000000..a866c61 --- /dev/null +++ b/src/main/resources/static/js/card/card-detail.js @@ -0,0 +1,38 @@ +// Edit button functionality +const editButton = document.querySelector('.edit-button'); +if (editButton) { + editButton.addEventListener('click', function(event) { + const cardId = this.getAttribute("data-id"); + console.log(cardId) + window.location.href = `/cards/${cardId}/edit`; // Redirect to edit page + }); +} + +// Delete button functionality +const deleteButton = document.querySelector('.delete-button'); +if (deleteButton) { + deleteButton.addEventListener('click', function(event) { + const cardId = this.getAttribute("data-id"); + + if (confirm("정말로 이 명함을 삭제하시겠습니까?")) { + fetch(`/cards/${cardId}`, { + method: "DELETE", + headers: { + "Content-Type": "application/json" + }, + }) + .then(response => { + if (response.ok) { + handleSuccess("명함이 삭제되었습니다.", 400); + window.location.href = `/cards/manage` + } else { + handleError("명함 삭제에 실패했습니다.", 300); + } + }) + .catch(error => { + console.error("명함 삭제 실패:", error); + handleError("명함 삭제에 실패했습니다.", 300); + }); + } + }); +} diff --git a/src/main/resources/static/js/card/card-edit.js b/src/main/resources/static/js/card/card-edit.js new file mode 100644 index 0000000..db9c3ce --- /dev/null +++ b/src/main/resources/static/js/card/card-edit.js @@ -0,0 +1,76 @@ +document.addEventListener("DOMContentLoaded", function () { + const cardId = window.location.pathname.split("/")[2]; + + // 기존 데이터 가져오기 + fetch(`/cards/${cardId}`, { + method: "GET", + headers: { + "Content-Type": "application/json" + }, + }) + .then(response => response.json()) + .then(data => { + document.getElementById("company").value = data.company || ""; + document.getElementById("position").value = data.position || ""; + document.getElementById("phone").value = data.phone || ""; + document.getElementById("cardName").value = data.nickname || data.name || ""; // 둘 다 없으면 빈 값 + document.getElementById("email").value = data.email || ""; + document.getElementById("profileImg").value = data.profileImg || ""; + document.getElementById("bio").value = data.bio || ""; + document.getElementById("linkedin").value = data.linkedin || ""; + document.getElementById("notion").value = data.notion || ""; + document.getElementById("certification").value = data.certification || ""; + document.getElementById("extra").value = data.extra || ""; + document.getElementById("techStack").checked = data.techStack || false; + document.getElementById("repository").checked = data.repository || false; + document.getElementById("contributions").checked = data.contributions || false; + }) + .catch(error => { + console.error("명함 데이터 로딩 실패:", error); + handleError("명함 데이터를 불러오는 데 실패했습니다.", 3000); + }); + + // 수정하기 버튼 클릭 시 + document.getElementById("card-edit-form").addEventListener("submit", function (e) { + e.preventDefault(); + + const updatedData = { + company: document.getElementById("company").value, + position: document.getElementById("position").value, + phone: document.getElementById("phone").value, + cardName: document.getElementById("cardName").value, + email: document.getElementById("email").value, + profileImg: document.getElementById("profileImg").value, + bio: document.getElementById("bio").value, + linkedin: document.getElementById("linkedin").value, + notion: document.getElementById("notion").value, + certification: document.getElementById("certification").value, + extra: document.getElementById("extra").value, + techStack: document.getElementById("techStack").checked, + repository: document.getElementById("repository").checked, + contributions: document.getElementById("contributions").checked, + }; + + fetch(`/cards/${cardId}`, { + method: "PUT", + headers: { + "Content-Type": "application/json" + }, + body: JSON.stringify(updatedData) + }) + .then(response => { + if (response.ok) { + handleSuccess("명함이 성공적으로 수정되었습니다!", 3000); + setTimeout(() => { + window.location.href = "/cards/manage"; + }, 300); // 토스트가 사라진 후 페이지 이동 + } else { + handleError("명함 수정에 실패했습니다.", 3000); + } + }) + .catch(error => { + console.error("명함 수정 실패:", error); + handleError("명함 수정에 실패했습니다.", 3000); + }); + }); +}); diff --git a/src/main/resources/static/js/card/card-list-delete.js b/src/main/resources/static/js/card/card-list-delete.js new file mode 100644 index 0000000..73a7f46 --- /dev/null +++ b/src/main/resources/static/js/card/card-list-delete.js @@ -0,0 +1,24 @@ +function deleteCardFromGroup(button, event) { + event.stopPropagation(); + + const cardId = button.getAttribute('card-id'); + const groupId = getCurrentGroupId(); + + if (confirm("정말로 이 명함을 그룹에서 삭제하시겠습니까?")) { + $.ajax({ + url: `/groups/${groupId}/cards/${cardId}/delete`, + type: 'DELETE', + success: function() { + alert("명함이 그룹에서 삭제되었습니다."); + location.reload(); + }, + error: function() { + alert("명함 삭제에 실패했습니다."); + } + }); + } +} + +function getCurrentGroupId() { + return window.location.pathname.split("/")[2]; // URL 경로에서 그룹 ID 추출 +} diff --git a/src/main/resources/static/js/card/card-manage.js b/src/main/resources/static/js/card/card-manage.js new file mode 100644 index 0000000..8dfae89 --- /dev/null +++ b/src/main/resources/static/js/card/card-manage.js @@ -0,0 +1,42 @@ +// card-manage.js + +document.addEventListener("DOMContentLoaded", function () { + function fetchCardList() { + fetch("/cards/my", { + method: "GET", + headers: { + "Content-Type": "application/json" + }, + }) + .then(response => response.json()) + .then(data => populateCardListSection(data)) + .catch(error => console.error("명함 목록 로딩 실패:", error)); + } + + // card-list-section을 채우는 함수 + function populateCardListSection(data) { + const cardListSection = document.getElementById("card-list-section"); + cardListSection.innerHTML = ''; // 기존 내용을 초기화 + + data.forEach(card => { + const cardContainer = document.createElement("div"); + cardContainer.className = "card-item"; + cardContainer.id = `card-${card.id}`; + + // 개별 카드 정보를 표시하기 위해 card.js의 populateCardSection 함수를 호출하여 HTML을 생성 + populateCardSection(card, cardContainer); + + // 클릭 이벤트 리스너 추가: 카드 클릭 시 해당 cardId로 상세 페이지 이동 + cardContainer.addEventListener("click", () => { + const cardId = card.id || 1; // cardId가 없을 경우 1로 설정 + window.location.href = `/cards/${cardId}/view`; + }); + + cardListSection.appendChild(cardContainer); + }); + } + + // 카드 목록을 가져옴 + fetchCardList(); +}); + diff --git a/src/main/resources/static/js/card/card-share.js b/src/main/resources/static/js/card/card-share.js new file mode 100644 index 0000000..8487b32 --- /dev/null +++ b/src/main/resources/static/js/card/card-share.js @@ -0,0 +1,75 @@ +// Kakao 공유 버튼 +document.getElementById('kakao-share-btn').addEventListener('click', function () { + if (!cardId) { + console.error('Card ID가 제공되지 않았습니다.'); + handleError('Card ID가 유효하지 않습니다.', 300); + return; + } + + // Kakao 공유 설정 + const content = { + title: `${cardName}님의 명함`, + imageUrl: 'https://developers.kakao.com/assets/img/about/logos/kakaolink/kakaolink_btn_medium.png', + link: { + mobileWebUrl: `http://3.34.144.148:8080/shared/cards/${cardId}`, + webUrl: `http://3.34.144.148:8080/shared/cards/${cardId}` + } + }; + + if (cardCompany || cardPosition) { + content.description = `회사: ${cardCompany || ''}${cardCompany && cardPosition ? ', ' : ''}직책: ${cardPosition || ''}`; + } + + Kakao.Link.sendDefault({ + objectType: 'feed', + content: content, + buttons: [ + { + title: '명함 보기', + link: { + mobileWebUrl: `http://3.34.144.148:8080/shared/cards/${cardId}`, + webUrl: `http://3.34.144.148:8080/shared/cards/${cardId}` + } + } + ], + fail: function (error) { + console.error(error); + handleError('카카오톡 공유에 실패했습니다.', 300); + } + }); +}); + +// QR 생성 및 렌더링 +document.getElementById('qr-share-btn').addEventListener('click', function () { + const cardId = this.getAttribute('data-card-id'); // 버튼에서 cardId 가져오기 + + if (!cardId) { + console.error('Card ID가 제공되지 않았습니다.'); + handleError('Card ID가 유효하지 않습니다.', 300); + return; + } + + const qrContainer = document.getElementById('qr-image-container'); + qrContainer.innerHTML = ''; // 이전 QR 코드 제거 + + // QR 코드 API 요청 + fetch(`/cards/${cardId}/qrcode-image?t=${new Date().getTime()}`) + .then(response => { + if (!response.ok) throw new Error('QR 코드 생성에 실패했습니다.'); + return response.blob(); + }) + .then(blob => { + const qrImage = document.createElement('img'); + qrImage.src = URL.createObjectURL(blob); + qrImage.alt = 'QR Code'; + qrImage.style.width = '200px'; + qrImage.style.height = '200px'; + + qrContainer.appendChild(qrImage); // QR 이미지 추가 + qrContainer.style.display = 'block'; // QR 컨테이너 표시 + }) + .catch(error => { + console.error('QR 코드 생성 중 오류:', error); + handleError('QR 코드 생성에 실패했습니다.', 400); + }); +}); diff --git a/src/main/resources/static/js/card/card.js b/src/main/resources/static/js/card/card.js new file mode 100644 index 0000000..6f9ab3d --- /dev/null +++ b/src/main/resources/static/js/card/card.js @@ -0,0 +1,64 @@ +function populateCardSection(data, container) { + if (data) { + const githubUsername = data.name || data.nickname; + const githubLink = githubUsername ? `https://github.com/${githubUsername}` : '#'; + + // 카드 요소 생성 + const cardElement = document.createElement("div"); + cardElement.classList.add("card"); + cardElement.id = `card-${data.id}`; + + cardElement.innerHTML = ` + +
+ Person profile +
+
+

${data.nickname || data.name}

+ ${data.company ? `

🏢 Company: ${data.company}

` : ''} + ${data.position ? `

📌 Position: ${data.position}

` : ''} + ${data.phone ? `

📞 Phone: ${data.phone}

` : ''} +

💻 GitHub: + + ${githubUsername || 'GitHub Profile'} + +

+ ${data.email ? `

📧 Email: ${data.email}

` : ''} + ${data.linkedin ? `

🔗 LinkedIn ${data.linkedin}

` : ''} + ${data.notion ? `

📚 Notion ${data.notion}

` : ''} + ${data.certification ? `

📜 Certification: ${data.certification}

` : ''} + ${data.extra ? `

📝 Extra: ${data.extra}

` : ''} + ${data.bio ? `

📝 Bio: ${data.bio}

` : ''} + ${data.techStack ? `

✅ Tech Stack Included

` : ''} + ${data.repository ? `

✅ Repository Included

` : ''} + ${data.contributions ? `

✅ Contributions Included

` : ''} +
+ `; + + // 클릭 이벤트 리스너 추가 + cardElement.addEventListener("click", () => { + window.location.href = `/cards/${data.id}/view`; + }); + + // 컨테이너에 카드 요소 추가 + container.appendChild(cardElement); + } else { + container.innerHTML = `

Card data not available.

`; + } +} + +// 개별 카드 데이터 가져오기 함수 (이 함수는 개별 카드 페이지에서만 사용됩니다) +document.addEventListener("DOMContentLoaded", function () { + const cardSection = document.getElementById("cardSection"); + if (cardSection) { + const cardId = cardSection.getAttribute("data-card-id") || 1; + fetchCardData(cardId, cardSection); + } +}); + +function fetchCardData(cardId, container) { + fetch(`/cards/${cardId}`) + .then(response => response.json()) + .then(data => populateCardSection(data, container)) + .catch(error => console.error('Error fetching card data:', error)); +} \ No newline at end of file diff --git a/src/main/resources/static/js/chat.js b/src/main/resources/static/js/chat.js index f0b650c..ccaa441 100644 --- a/src/main/resources/static/js/chat.js +++ b/src/main/resources/static/js/chat.js @@ -1,7 +1,4 @@ $(document).ready(function () { - $(".nav-item").removeClass("active"); // 모든 nav-item에서 active 클래스 제거 - $(".nav-item[data-page='chats']").addClass("active"); // chats에 active 클래스 추가 - const path = window.location.pathname; // 특정 사용자 정보를 가져오는 함수 @@ -13,6 +10,7 @@ $(document).ready(function () { callback(profile); }, error: function (error) { + handleError("프로필 정보를 불러오지 못했습니다."); console.error("프로필 정보를 불러오는데 오류가 발생했습니다:", error); } }); @@ -33,6 +31,7 @@ $(document).ready(function () { renderChatRooms(chatRooms); }, error: function (error) { + handleError("채팅방 목록을 불러오지 못했습니다."); console.error("채팅방 목록을 불러오는데 오류가 발생했습니다:", error); } }); @@ -50,7 +49,7 @@ $(document).ready(function () { return new Promise((resolve) => { fetchUserProfile(participantId, function(profile) { // 검색어가 포함된 경우 하이라이트 처리 - const highlightedName = highlightText(profile.nickname, searchTerm); + const highlightedName = highlightText(profile.name, searchTerm); const highlightedMessage = highlightText(lastMessage, searchTerm); const chatItem = $('
', {class: 'chat-item'}); @@ -149,7 +148,7 @@ $(document).ready(function () { fetchChatRoom(chatId); // 임시로 로컬 서버 설정 - const socket = new WebSocket(`ws://localhost:8080/ws?chatId=${chatId}&userId=${memberId}`); + const socket = new WebSocket(`ws://3.34.144.148:8080/ws?chatId=${chatId}&userId=${memberId}`); // 웹소켓 연결 socket.addEventListener("open", () => { @@ -158,6 +157,7 @@ $(document).ready(function () { // 웹소켓 예외처리 socket.addEventListener("error", (error) => { + handleError("서버와 연결 중 오류가 발생했습니다."); console.error("웹소켓 연결 중 오류가 발생:", error); }); @@ -172,7 +172,7 @@ $(document).ready(function () { function sendMessage(messageContent) { // 메시지 길이 체크 if (messageContent.length > 2000) { - console.log("메시지 최대 길이 초과"); + handleError("보낼 수 있는 메시지의 최대 길이는 2,000자입니다."); return; } @@ -228,6 +228,7 @@ $(document).ready(function () { renderChatRoom(chatRoom); }, error: function (error) { + handleError("채팅방 정보를 불러오지 못했습니다."); console.error("채팅방 정보를 불러오는데 오류가 발생했습니다:", error); } }); @@ -243,7 +244,7 @@ $(document).ready(function () { if (!receiverName) { // 프로필 닉네임을 비동기로 가져오고 receiverName 저장 fetchUserProfile(participantId, function(profile) { - receiverName = profile.nickname; + receiverName = profile.name; renderMessages(chatRoom.messages); $("#roomTitle").text(receiverName); }); diff --git a/src/main/resources/static/js/oauth/login.js b/src/main/resources/static/js/oauth/login.js new file mode 100644 index 0000000..0942a17 --- /dev/null +++ b/src/main/resources/static/js/oauth/login.js @@ -0,0 +1,3 @@ +function githubLogin() { + window.location.href = '/oauth2/authorization/github'; +} diff --git a/src/main/resources/static/js/oauth/redirect.js b/src/main/resources/static/js/oauth/redirect.js new file mode 100644 index 0000000..e3b6685 --- /dev/null +++ b/src/main/resources/static/js/oauth/redirect.js @@ -0,0 +1,14 @@ +fetch('/register', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + } +}) + .then(response => response.json()) + .then(data => { + window.location.href = "/home"; + }) + .catch(error => { + console.error('Error:', error); + window.location.href = "/login"; // 오류 발생 시 /login으로 redirect + }); diff --git a/src/main/resources/static/js/script.js b/src/main/resources/static/js/script.js new file mode 100644 index 0000000..d3d6726 --- /dev/null +++ b/src/main/resources/static/js/script.js @@ -0,0 +1,65 @@ +$(document).ready(function () { + // navBar active 조정 + const path = window.location.pathname; + + $(".nav-item").removeClass("active"); // 모든 nav-item에서 active 클래스 제거 + + if (path === '/home') { + $(".nav-item[data-page='home']").addClass("active"); // MyCard에 active 클래스 추가 + } else if (path === '/wallet-list' || path.startsWith('/groups')) { + $(".nav-item[data-page='wallet']").addClass("active"); // Wallet에 active 클래스 추가 + } else if (path === '/chats') { + $(".nav-item[data-page='chats']").addClass("active"); // Chats에 active 클래스 추가 + } else if (path.startsWith('/mypage') || path.startsWith('/cards')) { + $(".nav-item[data-page='mypage']").addClass("active"); // MyPage에 active 클래스 추가 + } +}); + + +// Toast 알림 +function showToast(message, type, duration) { + const $toast = $("#toast"); + + // 이미 표시 중인 알림이 있을 경우 한 번 닫기 + if ($toast.hasClass("show")) { + $toast.animate({ opacity: 0, bottom: '10%' }, 500, () => { + $toast.removeClass("show error success"); + // 알림을 숨긴 후 새 메시지로 다시 showToast 실행 + displayToast(message, type, duration); + }); + } else { + displayToast(message, type, duration); + } +} + +function displayToast(message, type, duration) { + const $toast = $("#toast"); + $toast.text(message); + + // 오류 또는 정상 동작 클래스 추가 + if (type === "error") { + $toast.removeClass("success").addClass("show error"); + } else if (type === "success") { + $toast.removeClass("error").addClass("show success"); + } else { + $toast.addClass("show"); + } + + // 초기 fade-in 애니메이션 + $toast.css({ opacity: 0, bottom: '10%' }).animate({ opacity: 1, bottom: '13%' }, 500); + + // 지정된 시간 후에 알림 숨김 및 fade-out 애니메이션 + setTimeout(() => { + $toast.animate({ opacity: 0, bottom: '10%' }, 500, () => { + $toast.removeClass("show error success"); + }); + }, duration); +} + +function handleError(message, duration = 3000) { + showToast(message, "error", duration); +} + +function handleSuccess(message, duration = 3000) { + showToast(message, "success", duration); +} diff --git a/src/main/resources/static/js/wallet/wallet-add.js b/src/main/resources/static/js/wallet/wallet-add.js new file mode 100644 index 0000000..39ee96b --- /dev/null +++ b/src/main/resources/static/js/wallet/wallet-add.js @@ -0,0 +1,40 @@ +document.addEventListener("DOMContentLoaded", function() { + const addToGroupButton = document.getElementById('add-to-group-btn'); + const groupModal = document.getElementById('group-modal'); + const modalOverlay = document.getElementById('modal-overlay'); + + // 현재 페이지의 명함 ID를 가져오기 + const cardId = document.getElementById('qr-share-btn').getAttribute('data-card-id'); + + // 그룹에 추가 버튼 클릭 시 모달 표시 + addToGroupButton?.addEventListener('click', function() { + groupModal.classList.add('visible') + modalOverlay.classList.add('visible'); + }); + + // 그룹 선택 시 명함을 해당 그룹에 추가 + document.querySelectorAll('.group-option').forEach(function(button) { + button.addEventListener('click', function() { + const groupId = this.getAttribute('data-group-id'); + + // AJAX 요청으로 그룹에 명함 추가 + $.ajax({ + url: `/groups/${groupId}/cards/${cardId}`, + type: 'POST', + success: function() { + alert('그룹에 명함이 추가되었습니다.'); + groupModal.style.display = 'none'; + }, + error: function(jqXHR) { + try { + const response = JSON.parse(jqXHR.responseText); + const errorMessage = response.error || '명함 추가에 실패했습니다.'; + alert(errorMessage); + } catch (e) { + alert('명함 추가에 실패했습니다.'); + } + } + }); + }); + }); +}); diff --git a/src/main/resources/static/js/wallet/wallet-list.js b/src/main/resources/static/js/wallet/wallet-list.js index 679ad5f..8e6f1be 100644 --- a/src/main/resources/static/js/wallet/wallet-list.js +++ b/src/main/resources/static/js/wallet/wallet-list.js @@ -1,3 +1,22 @@ +let isEditing = false; + +document.addEventListener("DOMContentLoaded", function() { + // 각 그룹 항목에 클릭 이벤트 추가 + document.querySelectorAll('.group-item').forEach(item => { + item.addEventListener('click', function(event) { + if (isEditing) return; + + // "수정" 버튼을 클릭한 경우에는 redirectToGroup을 실행하지 않음 + if (!event.target.classList.contains('edit-button') + && !event.target.classList.contains('delete-button') + && !event.target.classList.contains('group-name-input')) { + const groupId = this.getAttribute('data-group-id'); + redirectToGroup(groupId); + } + }); + }); +}); + function createNewGroup() { fetch('/groups', { method: 'POST', @@ -21,5 +40,70 @@ function createNewGroup() { } function redirectToGroup(groupId) { - window.location.href = `/groups/${groupId}/cards`; // 원하는 URL로 리다이렉트 -} \ No newline at end of file + window.location.href = `/groups/${groupId}/cards`; +} + +let currentGroupId = null; + +function toggleEditMode(button) { + const groupId = button.getAttribute('data-group-id'); + const groupItem = button.closest('.group-item'); + const groupNameSpan = groupItem.querySelector('.group-name'); + const groupNameInput = groupItem.querySelector('.group-name-input'); + + if (button.textContent === "수정") { + // "수정" 모드로 전환 + isEditing = true; + button.textContent = "완료"; + button.classList.add("editing"); // 수정 모드 색상 변경 + groupNameSpan.style.display = "none"; + groupNameInput.style.display = "inline-block"; + groupNameInput.focus(); + } else { + // "완료" 버튼 클릭 시 + isEditing = false; + const newName = groupNameInput.value.trim(); + if (newName === "") { + alert("그룹 이름을 입력하세요."); + return; + } + + // 서버로 수정 요청 + $.ajax({ + url: `/groups/${groupId}/update`, + type: 'POST', + contentType: 'application/json', + data: JSON.stringify({ name: newName }), + success: function() { + alert("그룹 이름이 수정되었습니다."); + groupNameSpan.textContent = newName; // UI에 반영 + button.textContent = "수정"; + button.classList.remove("editing"); // 색상 원래대로 + groupNameSpan.style.display = "inline-block"; + groupNameInput.style.display = "none"; + }, + error: function() { + alert("그룹 이름 수정에 실패했습니다."); + } + }); + } +} + +function deleteGroup(button) { + const groupId = button.getAttribute('data-group-id'); + + if (confirm("정말로 이 그룹을 삭제하시겠습니까?")) { + $.ajax({ + url: `/groups/${groupId}/delete`, // 그룹 삭제 엔드포인트 + type: 'DELETE', + success: function() { + alert("그룹이 삭제되었습니다."); + location.reload(); // 페이지 새로고침으로 삭제된 그룹 반영 + }, + error: function() { + alert("그룹 삭제에 실패했습니다."); + } + }); + } +} + diff --git a/src/main/resources/templates/card-create.html b/src/main/resources/templates/card-create.html new file mode 100644 index 0000000..62b65eb --- /dev/null +++ b/src/main/resources/templates/card-create.html @@ -0,0 +1,66 @@ + + + + + + 명함 생성 + + + + + + + + + + + + +
+ arrow_back_ios +

명함 생성

+
+ +
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +
+
+
+ +
+ + +
+ + + diff --git a/src/main/resources/templates/card-detail.html b/src/main/resources/templates/card-detail.html index 380ceb4..4f784e6 100644 --- a/src/main/resources/templates/card-detail.html +++ b/src/main/resources/templates/card-detail.html @@ -5,81 +5,107 @@ [[${card.name}]]님의 명함 - + + + + + + + + -
-
-

[[${card.name}]]

-

회사: [[${card.company}]]

-

직책: [[${card.position}]]

-

전화번호: [[${card.phone}]]

-

이메일: [[${card.email}]]

-

GitHub: [[${card.githubId}]]

-

소개: [[${card.bio}]]

-
- + + +
+

명함

-