Skip to content

Commit

Permalink
feature : [게시글 상세 조회] 상단 바 컴포넌트 (#91)
Browse files Browse the repository at this point in the history
* feat : 게시글 상세조회 상단 바 팝업 구현

- 수정 클릭 시 새 페이지로 이동
- 삭제 클릭 시 API 호출 및 모달 창 띄우기

* feat : 게시글 삭제 컨트롤러 테스트 작성

* refactor : 게시글 삭제 시 ArticleDetailScreen 띄우기

* refactor : 1차 리뷰 반영

- truncate 추가
- state 분리

* chore : develop과 병합
  • Loading branch information
jnsorn authored Aug 13, 2020
1 parent 4c87ec2 commit d2fb543
Show file tree
Hide file tree
Showing 12 changed files with 202 additions and 19 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,8 @@ public void update(Long id, ArticleRequest request) {
.orElseThrow(NoSuchElementException::new);
article.update(request.toArticle());
}

public void deleteById(Long id) {
articleRepository.deleteById(id);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
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;
Expand Down Expand Up @@ -65,6 +66,12 @@ public ResponseEntity<ArticleResponse> showArticle(@PathVariable Long id) {
return ResponseEntity.ok(articleResponse);
}

@DeleteMapping("/{id}")
public ResponseEntity<Void> delete(@PathVariable Long id) {
articleService.deleteById(id);
return ResponseEntity.noContent().build();
}

/**
* MemberResponse.of의 인자로 Favorite도 받도록 변경되어서 일단 주석처리했음
* 코즈 PR이 머지되면 수정할 예정
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/**
* @author kouz95
* @author jnsorn
*/

package sellerlee.back.article.acceptance;
Expand Down Expand Up @@ -64,6 +64,9 @@ Stream<DynamicTest> manageArticle() throws JsonProcessingException {
ArticleResponse articleResponse = findArticleDetailOf(articleId);
assertThat(articleResponse.getId()).isEqualTo(articleId);
assertThat(articleResponse.getFavoriteState()).isFalse();
}),
dynamicTest("게시글 삭제", () -> {
deleteArticle(articleId);
}));
}

Expand All @@ -90,4 +93,15 @@ private ArticleResponse findArticleDetailOf(Long articleId) {
.statusCode(HttpStatus.OK.value())
.extract().jsonPath().getObject(".", ArticleResponse.class);
}

private void deleteArticle(Long articleId) {
String url = ARTICLE_URI + "/" + articleId;

given()
.when()
.delete(url)
.then()
.log().all()
.statusCode(HttpStatus.NO_CONTENT.value());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -87,4 +87,12 @@ void update() {
assertThat(ARTICLE1.getTitle()).isEqualTo(request.getTitle());
assertThat(ARTICLE1.getContents()).isEqualTo(request.getContents());
}

@DisplayName("게시글 삭제 메서드 호출 시 게시글 삭제")
@Test
void deleteArticle() {
articleService.deleteById(ARTICLE1.getId());

verify(articleRepository).deleteById(ARTICLE1.getId());
}
}
Empty file.
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,16 @@ void showArticle() throws Exception {
.andExpect(status().isOk());
}

@DisplayName("게시글 삭제 시 HTTP status는 noContent다.")
@Test
void deleteArticle() throws Exception {
mockMvc.perform(delete(ARTICLE_URI + "/" + ARTICLE1.getId()))
.andDo(print())
.andExpect(status().isNoContent());

verify(articleService).deleteById(ARTICLE1.getId());
}

// @DisplayName("게시글 판매 상태로 게시글 조회 시 HTTP STATUS OK와 판매 상태에 해당하는 게시글 반환")
// @Test
// void showArticlesByTradeState() throws Exception {
Expand Down
11 changes: 7 additions & 4 deletions front/App.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,23 @@
/**
* @author kouz95
* @author jnsorn
*/

import React from "react";
import { NavigationContainer } from "@react-navigation/native";
import { RecoilRoot } from "recoil/dist";
import BottomTabNavigation from "./src/components/Navigation/BottomTabNavigation";
import { MenuProvider } from "react-native-popup-menu";

console.disableYellowBox = true;

export default function App() {
return (
<RecoilRoot>
<NavigationContainer>
<BottomTabNavigation />
</NavigationContainer>
<MenuProvider>
<NavigationContainer>
<BottomTabNavigation />
</NavigationContainer>
</MenuProvider>
</RecoilRoot>
);
}
1 change: 1 addition & 0 deletions front/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
"react-native-gesture-handler": "~1.6.1",
"react-native-keyboard-aware-scroll-view": "^0.9.1",
"react-native-modal": "^11.5.6",
"react-native-popup-menu": "^0.15.9",
"react-native-reanimated": "~1.9.0",
"react-native-safe-area-context": "~3.0.7",
"react-native-screens": "~2.9.0",
Expand Down
2 changes: 2 additions & 0 deletions front/src/api/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ export const articlesAPI = {
await axios.post(`${BASE_URL}${domain.article}`, data),
put: async (articleId: number, data: ArticlesPost) =>
await axios.put(`${BASE_URL}${domain.article}/${articleId}`, data),
delete: async (articleId: number) =>
await axios.delete(`${BASE_URL}${domain.article}/${articleId}`),
};

export const articleDetailAPI = {
Expand Down
76 changes: 76 additions & 0 deletions front/src/components/Common/Modal/ArticleDeleteModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import React from "react";
import { Modal, View, StyleSheet, Text, Button } from "react-native";
import { useRecoilState } from "recoil/dist";
import { articleDetailModalState } from "../../../states/modalState";
import theme from "../../../colors";
import { useNavigation } from "@react-navigation/native";
import { ArticleDetailNavigationProp } from "../../../types/types";

export default function ArticleDeleteModal() {
const navigation = useNavigation<ArticleDetailNavigationProp>();
const [modalVisible, setModalVisible] = useRecoilState(
articleDetailModalState,
);

return (
<Modal
animationType="fade"
transparent={true}
visible={modalVisible}
onRequestClose={() => setModalVisible(false)}
>
<View style={styles.centeredView}>
<View style={styles.modalView}>
<Text style={styles.modalText}> 삭제되었습니다. </Text>
<Button
title={"목록으로 이동"}
onPress={() => {
setModalVisible(false);
navigation.navigate("FeedHome");
}}
color={theme.primary}
/>
</View>
</View>
</Modal>
);
}

const styles = StyleSheet.create({
centeredView: {
flex: 1,
justifyContent: "center",
alignItems: "center",
marginTop: 22,
},
modalView: {
margin: 20,
backgroundColor: "#fff",
borderRadius: 20,
padding: 25,
alignItems: "center",
shadowColor: "#000",
shadowOffset: {
width: 3,
height: 5,
},
shadowOpacity: 0.25,
shadowRadius: 3.84,
elevation: 3,
},
closeButton: {
backgroundColor: "#dfd3c3",
borderRadius: 20,
padding: 10,
elevation: 2,
},
textStyle: {
color: "white",
fontWeight: "bold",
textAlign: "center",
},
modalText: {
marginBottom: 15,
textAlign: "center",
},
});
81 changes: 67 additions & 14 deletions front/src/screens/ArticleDetailScreen.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import React, { useEffect, useLayoutEffect } from "react";
import { Button, ScrollView, StyleSheet, View } from "react-native";
import React, { useEffect, useLayoutEffect, useState } from "react";
import { ScrollView, StyleSheet, View } from "react-native";
import { useNavigation } from "@react-navigation/native";
import { HeaderBackButton } from "@react-navigation/stack";
import { EvilIcons } from "@expo/vector-icons";
import { EvilIcons, Ionicons } from "@expo/vector-icons";
import ArticleDetail from "../components/ArticleDetail/ArticleDetail";
import ArticleDetailFavorite from "../components/ArticleDetail/ArticleDetailFavorite";
import { ArticleDetailNavigationProp } from "../types/types";
Expand All @@ -11,33 +11,57 @@ import ArticleDetailChatButton from "../components/ArticleDetail/ArticleDetailCh
import ArticleDetailImageSlider from "../components/ArticleDetail/ArticleDetailImageSlider";
import ArticleAuthor from "../components/Article/ArticleAuthor";
import theme from "../colors";
import { articleDetailAPI } from "../api/api";
import { articleDetailAPI, articlesAPI } from "../api/api";
import {
Menu,
MenuOption,
MenuOptions,
MenuTrigger,
} from "react-native-popup-menu";

import { useRecoilValue, useSetRecoilState } from "recoil/dist";
import {
articleIsEditingState,
articleSelectedIdState,
articleSelectedState,
} from "../states/articleState";
import { articleDetailModalState } from "../states/modalState";
import ArticleDeleteModal from "../components/Common/Modal/ArticleDeleteModal";

export default function ArticleDetailScreen() {
const [currentY, setCurrentY] = useState(0);
const setModalVisible = useSetRecoilState(articleDetailModalState);
const navigation = useNavigation<ArticleDetailNavigationProp>();
const articleId = useRecoilValue(articleSelectedIdState);
const setArticleSelected = useSetRecoilState(articleSelectedState);
const setIsEditing = useSetRecoilState(articleIsEditingState);

const getArticle = async () => {
const { data } = await articleDetailAPI.get(articleId);
console.log(data);
setArticleSelected(data);
};

const dynamicStyles = StyleSheet.create({
headerBackground: {
height: 90,
backgroundColor: `rgba(255,255,255,${currentY / 70})`,
},
});

useEffect(() => {
navigation.setOptions({
headerBackground: () => <View style={dynamicStyles.headerBackground} />,
});
}, [currentY]);

useEffect(() => {
getArticle();
}, [articleId]);

useLayoutEffect(() => {
navigation.setOptions({
title: "",
headerTransparent: true,
headerLeft: () => (
<HeaderBackButton
labelVisible={false}
Expand All @@ -47,27 +71,49 @@ export default function ArticleDetailScreen() {
)}
/>
),
headerLeftContainerStyle: { paddingLeft: 10 },
headerRight: () => (
<Button
title="수정"
onPress={() => {
navigation.navigate("ArticleFormScreen");
setIsEditing(true);
}}
/>
<Menu>
<MenuTrigger>
<Ionicons name="md-more" size={26} color={"grey"} />
</MenuTrigger>
<MenuOptions
optionsContainerStyle={styles.menuOptionsContainer}
customStyles={{ optionText: styles.menuCustomText }}
>
<MenuOption
onSelect={() => {
navigation.navigate("ArticleFormScreen");
setIsEditing(true);
}}
text={"수정"}
/>
<MenuOption
onSelect={() =>
articlesAPI.delete(articleId).then(() => {
setModalVisible(true);
})
}
text={"삭제"}
/>
</MenuOptions>
</Menu>
),
headerLeftContainerStyle: { paddingLeft: 10 },
headerRightContainerStyle: { paddingRight: 15 },
});
}),
[navigation];
});

return (
<View style={styles.container}>
<ArticleDeleteModal />
<ScrollView
style={styles.scrollView}
bounces={false}
contentContainerStyle={styles.scrollViewContentContainer}
showsVerticalScrollIndicator={false}
onScroll={(event) => setCurrentY(event.nativeEvent.contentOffset.y)}
scrollEventThrottle={1}
>
<View style={styles.imageSliderContainer}>
<ArticleDetailImageSlider />
Expand Down Expand Up @@ -134,4 +180,11 @@ const styles = StyleSheet.create({
alignItems: "center",
marginRight: 10,
},
menuOptionsContainer: {
width: 80,
},
menuCustomText: {
textAlign: "center",
margin: 10,
},
});
5 changes: 5 additions & 0 deletions front/src/states/modalState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,8 @@ export const modalActivationState = atom({
key: "modalActivationState",
default: false,
});

export const articleDetailModalState = atom({
key: "articleDetailModalState",
default: false,
});

0 comments on commit d2fb543

Please sign in to comment.